import {
  mkdir,
  mkdtemp,
  readFile,
  readdir,
  rm,
  writeFile
} from 'node:fs/promises'
import { existsSync, readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
  ContinueHostAdapter,
  type ContinueHostLauncher
} from './continue-host-adapter'

const temporaryDirectories: string[] = []
const environmentRestorations: Array<() => void> = []

const inheritedProviderCredentials = {
  ANTHROPIC_API_KEY: 'inherited-anthropic',
  OPENAI_API_KEY: 'inherited-openai',
  GOOGLE_GENERATIVE_AI_API_KEY: 'inherited-google',
  GEMINI_API_KEY: 'inherited-gemini',
  AWS_ACCESS_KEY_ID: 'inherited-aws-access',
  AWS_SECRET_ACCESS_KEY: 'inherited-aws-secret',
  AWS_SESSION_TOKEN: 'inherited-aws-session',
  AWS_PROFILE: 'inherited-aws-profile',
  OPENROUTER_API_KEY: 'inherited-openrouter'
} as const

function inheritProviderCredentials(): void {
  const previousEnvironment = Object.fromEntries(
    Object.keys(inheritedProviderCredentials).map((name) => [
      name,
      process.env[name]
    ])
  )
  Object.assign(process.env, inheritedProviderCredentials)
  environmentRestorations.push(() => {
    for (const [name, value] of Object.entries(previousEnvironment)) {
      if (value === undefined) {
        delete process.env[name]
      } else {
        process.env[name] = value
      }
    }
  })
}

async function createDistribution(version = '1.5.47'): Promise<{
  cacheRoot: string
  entryPath: string
  sourceHash: string
}> {
  const root = await mkdtemp(join(tmpdir(), 'goodbuddy-continue-host-'))
  temporaryDirectories.push(root)
  const distribution = join(root, 'package', 'dist')
  const cacheRoot = join(root, 'cache')
  await mkdir(distribution, { recursive: true })
  await writeFile(
    join(root, 'package', 'package.json'),
    JSON.stringify({ version }),
    'utf8'
  )
  await writeFile(join(distribution, 'cn.js'), 'import "./index.js"\n', 'utf8')
  await writeFile(join(distribution, 'xhr-sync-worker.js'), '', 'utf8')
  const sourceBundle = [
    'toolPermissionOverrides:s,headless:!0});let[a,u,l,c]',
    'i={allow:o.allow,ask:o.ask,exclude:o.exclude,isHeadless:e.headless}',
    'E6t.initialize({isHeadless:e.headless},r,n)',
    'function ZZo(e){let t=[];if(e.exclude)for(let n of e.exclude){let r=n;t.push({tool:r,permission:"exclude"})}if(e.ask)for(let n of e.ask){let r=n;t.push({tool:r,permission:"ask"})}if(e.allow)for(let n of e.allow){let r=n;t.push({tool:r,permission:"allow"})}return t}',
    'let j=(0,atn.default)();j.use(atn.default.json()),j.get("/state"',
    'listen(i,async()=>{console.log(Ht.green(`Server started on http://localhost:${i}`))',
    'async function SCt(e){return n5e||',
    'shouldUseResponsesEndpoint(t){return this.config.useResponsesApi===!1?!1:this.apiBase==="https://api.openai.com/v1/"&&A0e(t)}',
    'function uAe(e,t){let n={provider:e.provider,model:e.model,apiKey:e.apiKey,apiBase:e.apiBase,requestOptions:e.requestOptions,env:e.env};return CGn(n)??null}',
    'function Sin(e,t){let n=[];n.push({role:"system",content:t});let r=oot(e);return n.push(...r),n}',
    'function Csa(e){return process.platform==="win32"?{shell:"powershell.exe",args:["-NoLogo","-ExecutionPolicy","Bypass","-Command",e]}',
    'let{shell:d,args:p}=Csa(e),f=Esa(d,p),g="",y="",A,S=!1,x=18e4;',
    'let r=[eS.join(n,".continue",AKt),eS.join(n,".claude",AKt),eS.join(hu.continueHome,AKt)],o=',
    'a={onContent:u=>{},onContentComplete:u=>{},onToolStart:(u,l)=>{},onToolResult:(u,l,c)=>{},onToolError:(u,l)=>{},onToolPermissionRequest:',
    'pendingPermission:null},B=',
    'j.get("/state",(we,Te)=>{M.lastActivity=Date.now(),B();let ue=e7e(M.session,M.isProcessing,rS.getQueueLength(),M.pendingPermission);Te.json(ue)})',
    'n?.onToolStart?.(i.name,i.arguments);',
    'n?.onToolError?.(l,i.name)',
    't?.onToolStart?.(c.name,c.arguments);',
    't?.onToolResult?.(String(y.content),c.name,"canceled")',
    't?.onToolResult?.(f,c.name,"done")',
    't?.onToolError?.(g,c.name)',
    't?.onToolError?.(p,c.name)'
  ].join(';')
  await writeFile(join(distribution, 'index.js'), sourceBundle, 'utf8')
  return {
    cacheRoot,
    entryPath: join(distribution, 'cn.js'),
    sourceHash: createHash('sha256')
      .update(sourceBundle)
      .digest('hex')
  }
}

afterEach(async () => {
  vi.unstubAllGlobals()
  for (const restoreEnvironment of environmentRestorations.splice(0)) {
    restoreEnvironment()
  }
  await Promise.all(
    temporaryDirectories.splice(0).map((directory) =>
      rm(directory, { recursive: true, force: true })
    )
  )
})

describe('ContinueHostAdapter', () => {
  it('creates a versioned authenticated loopback host copy', async () => {
    const distribution = await createDistribution()
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash]
    })

    const prepared = await adapter.getPreparedHost()
    const bundle = await readFile(
      join(prepared.entryPath, '..', 'index.js'),
      'utf8'
    )
    const bootstrap = await readFile(
      join(prepared.entryPath, '..', 'utility-bootstrap.mjs'),
      'utf8'
    )

    expect(prepared.version).toBe('1.5.47')
    expect(bundle).toContain('interactivePermissions:!0')
    expect(bundle).toContain(
      'isHeadless:e.interactivePermissions?!1:e.headless'
    )
    expect(bundle).toContain('GOODBUDDY_CONTINUE_HOST_TOKEN')
    expect(bundle).toContain('listen(i,"127.0.0.1"')
    expect(bundle).toContain(
      'GOODBUDDY_DISABLE_CONTINUE_UPDATES'
    )
    expect(bundle).toContain(
      'this.config.useResponsesApi===!0?!0'
    )
    expect(bundle).toContain(
      'useResponsesApi:e.useResponsesApi'
    )
    expect(bundle).toContain(
      'let r=oot(e).filter(o=>o.role!=="system")'
    )
    expect(bundle).toContain('"-NoProfile"')
    expect(bundle).toContain('[Console]::OutputEncoding')
    expect(bundle).toContain(
      'f.stdout.setEncoding("utf8"),f.stderr.setEncoding("utf8")'
    )
    expect(bundle).toContain(
      'let r=[eS.join(hu.continueHome,AKt)],o='
    )
    expect(bundle).toContain('goodbuddyEvents:[]')
    expect(bundle).toContain('goodbuddyEvents:ce')
    expect(bundle).toContain('type:"text",delta:u')
    expect(bundle).toContain('onToolStart?.(c.name,c.arguments,c.id)')
    expect(bundle).toContain(
      'function ZZo(e){let t=[];if(e.allow)'
    )
    expect(bundle).not.toContain(
      'toolPermissionOverrides:s,headless:!0});let'
    )
    expect(bootstrap).toContain(
      'process.argv = process.argv.slice(2)'
    )
  })

  it('rejects unsupported Continue versions without patching them', async () => {
    const distribution = await createDistribution('1.6.0')
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash]
    })

    await expect(adapter.getPreparedHost()).rejects.toThrow(
      '仅支持 1.5.47'
    )
  })

  it('rejects an untrusted bundle even when markers and version match', async () => {
    const distribution = await createDistribution()
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: ['0'.repeat(64)]
    })

    await expect(adapter.getPreparedHost()).rejects.toThrow(
      '兼容性校验'
    )
  })

  it('removes capability config when host preparation fails after generation', async () => {
    const distribution = await createDistribution()
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [],
      modelProfile: {
        id: '00000000-0000-4000-8000-000000000099',
        name: 'Local model',
        baseUrl: 'http://127.0.0.1:11434/v1',
        modelName: 'qwen3',
        protocol: 'openai-chat-completions',
        authentication: 'none'
      }
    })

    await expect(
      adapter.run(
        'search',
        new AbortController().signal,
        async () => 'deny',
        {
          workMode: 'ask',
          knowledgeCapability: {
            endpoint: 'http://127.0.0.1:4567/mcp',
            token: 'main-only-token'
          }
        }
      )
    ).rejects.toThrow('未通过宿主兼容性校验')
    await expect(readdir(distribution.cacheRoot)).resolves.not.toEqual(
      expect.arrayContaining([
        expect.stringMatching(/^model-config-/u)
      ])
    )
  })

  it('removes capability config when cancellation reaches the pre-spawn check', async () => {
    const distribution = await createDistribution()
    const launchHost = vi.fn<ContinueHostLauncher>()
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash],
      launchHost,
      modelProfile: {
        id: '00000000-0000-4000-8000-000000000098',
        name: 'Local model',
        baseUrl: 'http://127.0.0.1:11434/v1',
        modelName: 'qwen3',
        protocol: 'openai-chat-completions',
        authentication: 'none'
      }
    })
    const controller = new AbortController()
    const pending = adapter.run(
      'search',
      controller.signal,
      async () => 'deny',
      {
        workMode: 'ask',
        knowledgeCapability: {
          endpoint: 'http://127.0.0.1:4567/mcp',
          token: 'main-only-token'
        }
      }
    )
    setTimeout(() => controller.abort(new Error('cancelled')), 0)

    await expect(pending).rejects.toThrow('cancelled')
    expect(launchHost).not.toHaveBeenCalled()
    await expect(readdir(distribution.cacheRoot)).resolves.not.toEqual(
      expect.arrayContaining([
        expect.stringMatching(/^model-config-/u)
      ])
    )
  })

  it('blocks runs without an explicit model profile or config file', async () => {
    const launchHost = vi.fn()
    const adapter = new ContinueHostAdapter({
      binaryPath: 'C:\\unused\\cn.js',
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: 'C:\\unused\\cache',
      launchHost: launchHost as unknown as ContinueHostLauncher
    })

    await expect(
      adapter.run('hello', new AbortController().signal, async () => 'deny')
    ).rejects.toThrow('尚未配置模型连接')
    expect(launchHost).not.toHaveBeenCalled()
  })

  it('launches the prepared host through the injected launcher', async () => {
    const distribution = await createDistribution()
    const skillDirectory = join(
      distribution.cacheRoot,
      '..',
      'longdoc-docx'
    )
    await mkdir(skillDirectory, { recursive: true })
    await writeFile(
      join(skillDirectory, 'SKILL.md'),
      [
        '---',
        'name: longdoc-docx',
        'description: Build a long Word document',
        '---',
        '',
        '# Long document'
      ].join('\n'),
      'utf8'
    )
    await writeFile(
      join(skillDirectory, 'build.py'),
      'print("build")\n',
      'utf8'
    )
    let launch:
      | {
          entryPath: string
          args: string[]
          env: NodeJS.ProcessEnv
        }
      | undefined
    let killed = false
    let generatedConfig = ''
    let generatedConfigPath = ''
    let isolatedGlobalDirectory = ''
    let registeredSkill = ''
    let registeredSkillFile = ''
    const launchHost: ContinueHostLauncher = (
      entryPath,
      args,
      options
    ) => {
      launch = { entryPath, args, env: options.env }
      isolatedGlobalDirectory =
        options.env.CONTINUE_GLOBAL_DIR ?? ''
      registeredSkill = readFileSync(
        join(
          isolatedGlobalDirectory,
          'skills',
          'longdoc-docx',
          'SKILL.md'
        ),
        'utf8'
      )
      registeredSkillFile = readFileSync(
        join(
          isolatedGlobalDirectory,
          'skills',
          'longdoc-docx',
          'build.py'
        ),
        'utf8'
      )
      const configIndex = args.indexOf('--config')
      if (configIndex >= 0) {
        generatedConfigPath = args[configIndex + 1] ?? ''
        generatedConfig = readFileSync(generatedConfigPath, 'utf8')
      }
      return {
        exitCode: null,
        get killed() {
          return killed
        },
        stderr: null,
        once: () => undefined,
        kill: () => {
          killed = true
          return true
        }
      }
    }
    let stateRequests = 0
    vi.stubGlobal(
      'fetch',
      vi.fn(async (input: string | URL | Request) => {
        const url = String(input)
        if (url.endsWith('/state')) {
          stateRequests += 1
          return new Response(
            JSON.stringify({
              session: {
                history:
                  stateRequests === 1
                    ? []
                    : [
                        {
                          message: {
                            role: 'assistant',
                            content: 'HOST_LAUNCH_OK'
                          }
                        }
                      ],
                usage:
                  stateRequests === 1
                    ? {
                        promptTokens: 20,
                        completionTokens: 10,
                        promptTokensDetails: {
                          cachedTokens: 5,
                          cacheWriteTokens: 2
                        }
                      }
                    : {
                        promptTokens: 19,
                        completionTokens: 9,
                        promptTokensDetails: {
                          cachedTokens: 4,
                          cacheWriteTokens: 1
                        }
                      }
              },
              isProcessing: false,
              messageQueueLength: 0,
              pendingPermission: null
            })
          )
        }
        return new Response('{}')
      })
    )
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash],
      launchHost,
      mode: 'chat',
      skillPackages: [
        {
          id: 'longdoc-docx',
          directory: skillDirectory
        }
      ],
      modelProfile: {
        id: '00000000-0000-4000-8000-000000000011',
        name: '独立模型',
        baseUrl: 'https://model.example',
        modelName: 'private-model',
        protocol: 'anthropic-messages',
        authentication: 'api-key',
        apiKey: 'private-key'
      }
    })

    await expect(
      adapter.run('hello', new AbortController().signal, async () => 'deny')
    ).resolves.toEqual({
      text: 'HOST_LAUNCH_OK',
      usage: {
        provider: 'anthropic',
        model: 'private-model',
        inputTokens: 0,
        outputTokens: 0,
        cacheReadTokens: 0,
        cacheWriteTokens: 0
      }
    })
    expect(launch?.entryPath).toContain('host-v6')
    expect(launch?.args).toEqual([
      '--config',
      expect.stringContaining('model-config-'),
      '--readonly',
      'serve',
      '--port',
      expect.any(String),
      '--timeout',
      '300'
    ])
    expect(launch?.env.GOODBUDDY_CONTINUE_HOST_TOKEN).toEqual(
      expect.any(String)
    )
    expect(launch?.env).toMatchObject({
      CONTINUE_CLI_AUTO_UPDATED: '1',
      CONTINUE_CLI_ENABLE_TELEMETRY: '0',
      CONTINUE_METRICS_ENABLED: '0',
      CONTINUE_GLOBAL_DIR: expect.stringContaining('isolated-global'),
      DO_NOT_TRACK: '1',
      GOODBUDDY_DISABLE_CONTINUE_UPDATES: '1',
      OTEL_EXPORTER_OTLP_ENDPOINT: '',
      OTEL_EXPORTER_OTLP_HEADERS: '',
      OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: '',
      OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
      OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '',
      OTEL_LOGS_EXPORTER: 'none',
      OTEL_LOG_USER_PROMPTS: '0',
      OTEL_METRICS_EXPORTER: 'none',
      OTEL_SDK_DISABLED: 'true',
      OTEL_TRACES_EXPORTER: 'none'
    })
    if (process.platform === 'win32') {
      expect(launch?.env).toMatchObject({
        PYTHONIOENCODING: 'utf-8',
        PYTHONUTF8: '1'
      })
    }
    expect(registeredSkill).toContain('name: longdoc-docx')
    expect(registeredSkillFile).toBe('print("build")\n')
    expect(existsSync(isolatedGlobalDirectory)).toBe(false)
    expect(killed).toBe(true)
    expect(JSON.parse(generatedConfig)).toMatchObject({
      models: [
        {
          apiBase: 'https://model.example/v1',
          apiKey: '${{ secrets.ANTHROPIC_API_KEY }}',
          model: 'private-model'
        }
      ]
    })
    expect(generatedConfig).not.toContain('private-key')
    expect(launch?.env.ANTHROPIC_API_KEY).toBe('private-key')
    expect(existsSync(generatedConfigPath)).toBe(false)
  })

  it('injects scoped knowledge into a temporary copy of a JSONC config', async () => {
    const distribution = await createDistribution()
    const configPath = join(
      distribution.cacheRoot,
      '..',
      'continue.jsonc'
    )
    const originalConfig = [
      '{',
      '  // User-managed Continue configuration',
      '  "name": "Private Continue",',
      '  "version": "1.0.0",',
      '  "schema": "v1",',
      '  "models": [{ "provider": "ollama", "model": "qwen3" }],',
      '  "mcpServers": [{ "name": "user-tools", "command": "tool.exe" }],',
      '}'
    ].join('\n')
    await writeFile(configPath, originalConfig, 'utf8')
    let generatedConfig = ''
    let generatedConfigPath = ''
    let killed = false
    const launchHost: ContinueHostLauncher = (
      _entryPath,
      args
    ) => {
      const configIndex = args.indexOf('--config')
      generatedConfigPath = args[configIndex + 1] ?? ''
      generatedConfig = readFileSync(generatedConfigPath, 'utf8')
      expect(args).toEqual([
        '--config',
        expect.stringContaining('knowledge-config-'),
        '--allow',
        'knowledge_search',
        '--allow',
        'note_search',
        '--exclude',
        '*',
        'serve',
        '--port',
        expect.any(String),
        '--timeout',
        '300'
      ])
      return {
        exitCode: null,
        get killed() {
          return killed
        },
        stderr: null,
        once: () => undefined,
        kill: () => {
          killed = true
          return true
        }
      }
    }
    let stateRequests = 0
    vi.stubGlobal(
      'fetch',
      vi.fn(async (input: string | URL | Request) => {
        if (String(input).endsWith('/state')) {
          stateRequests += 1
          return Response.json({
            session: {
              history:
                stateRequests === 1
                  ? []
                  : [
                      {
                        message: {
                          role: 'assistant',
                          content: 'CONFIG_KNOWLEDGE_OK'
                        }
                      }
                    ]
            },
            isProcessing: false,
            messageQueueLength: 0,
            pendingPermission: null
          })
        }
        return Response.json({})
      })
    )
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath,
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash],
      launchHost,
      mode: 'agent'
    })

    await expect(
      adapter.run(
        'search',
        new AbortController().signal,
        async () => 'deny',
        {
          workMode: 'ask',
          knowledgeCapability: {
            endpoint: 'http://127.0.0.1:4567/mcp',
            token: 'main-only-token'
          }
        }
      )
    ).resolves.toEqual({ text: 'CONFIG_KNOWLEDGE_OK' })
    expect(JSON.parse(generatedConfig)).toMatchObject({
      name: 'Private Continue',
      models: [{ provider: 'ollama', model: 'qwen3' }],
      mcpServers: [
        {
          name: 'goodbuddy-knowledge',
          type: 'streamable-http',
          url: 'http://127.0.0.1:4567/mcp',
          requestOptions: {
            headers: {
              Authorization: 'Bearer main-only-token'
            }
          }
        }
      ]
    })
    expect(generatedConfig).not.toContain('user-tools')
    await expect(readFile(configPath, 'utf8')).resolves.toBe(
      originalConfig
    )
    expect(killed).toBe(true)
    expect(existsSync(generatedConfigPath)).toBe(false)
  })

  it.each([
    {
      label: 'Chat Completions without authentication',
      protocol: 'openai-chat-completions' as const,
      authentication: 'none' as const,
      useResponsesApi: false
    },
    {
      label: 'Responses with an API key',
      protocol: 'openai-responses' as const,
      authentication: 'api-key' as const,
      useResponsesApi: true
    }
  ])(
    'generates an explicit OpenAI config for $label',
    async ({
      protocol,
      authentication,
      useResponsesApi
    }) => {
      inheritProviderCredentials()
      const distribution = await createDistribution()
      let generatedConfig = ''
      let launchedEnvironment: NodeJS.ProcessEnv | undefined
      let launchedArgs: string[] = []
      const launchHost: ContinueHostLauncher = (
        _entryPath,
        args,
        options
      ) => {
        launchedArgs = args
        const configIndex = args.indexOf('--config')
        generatedConfig = readFileSync(
          args[configIndex + 1] ?? '',
          'utf8'
        )
        launchedEnvironment = options.env
        return {
          exitCode: null,
          killed: false,
          stderr: null,
          once: () => undefined,
          kill: () => true
        }
      }
      let stateRequests = 0
      vi.stubGlobal(
        'fetch',
        vi.fn(async (input: string | URL | Request) => {
          if (String(input).endsWith('/state')) {
            stateRequests += 1
            return Response.json({
              session: {
                history:
                  stateRequests === 1
                    ? []
                    : [
                        {
                          message: {
                            role: 'assistant',
                            content: 'OLLAMA_OK'
                          }
                        }
                      ],
                usage:
                  stateRequests === 1
                    ? {
                        promptTokens: 100,
                        completionTokens: 20,
                        promptTokensDetails: {
                          cachedTokens: 10,
                          cacheWriteTokens: 3
                        }
                      }
                    : {
                        promptTokens: 131,
                        completionTokens: 29,
                        promptTokensDetails: {
                          cachedTokens: 23,
                          cacheWriteTokens: 7
                        }
                      }
              },
              isProcessing: false,
              messageQueueLength: 0,
              pendingPermission: null
            })
          }
          return Response.json({})
        })
      )
      const adapter = new ContinueHostAdapter({
        binaryPath: distribution.entryPath,
        configPath: '',
        workspace: process.cwd(),
        cacheRoot: distribution.cacheRoot,
        trustedBundleHashes: [distribution.sourceHash],
        launchHost,
        modelProfile: {
          id: '00000000-0000-4000-8000-000000000012',
          name: 'Ollama',
          baseUrl: 'http://127.0.0.1:11434/v1',
          modelName: 'qwen3',
          protocol,
          authentication,
          ...(authentication === 'api-key'
            ? { apiKey: 'private-key' }
            : {})
        }
      })

      await expect(
        adapter.run(
          'hello',
          new AbortController().signal,
          async () => 'deny',
          {
            workMode: 'ask',
            knowledgeCapability: {
              endpoint: 'http://127.0.0.1:4567/mcp',
              token: 'main-only-token'
            }
          }
        )
      ).resolves.toEqual({
        text: 'OLLAMA_OK',
        usage: {
          provider: 'openai',
          model: 'qwen3',
          inputTokens: 31,
          outputTokens: 9,
          cacheReadTokens: 13,
          cacheWriteTokens: 4
        }
      })
      expect(JSON.parse(generatedConfig)).toMatchObject({
        models: [
          {
            provider: 'openai',
            apiBase: 'http://127.0.0.1:11434/v1',
            model: 'qwen3',
            useResponsesApi
          }
        ],
        mcpServers: [
          {
            name: 'goodbuddy-knowledge',
            type: 'streamable-http',
            url: 'http://127.0.0.1:4567/mcp',
            requestOptions: {
              headers: {
                Authorization: 'Bearer main-only-token'
              }
            }
          }
        ]
      })
      expect(launchedArgs).toEqual(
        expect.arrayContaining([
          '--allow',
          'knowledge_search',
          '--allow',
          'note_search',
          '--exclude',
          '*'
        ])
      )
      expect(launchedArgs).not.toContain('--readonly')
      if (authentication === 'api-key') {
        expect(JSON.parse(generatedConfig)).toMatchObject({
          models: [
            {
              apiKey: '${{ secrets.OPENAI_API_KEY }}'
            }
          ]
        })
        expect(launchedEnvironment?.OPENAI_API_KEY).toBe('private-key')
      } else {
        expect(generatedConfig).not.toContain('apiKey')
        expect(launchedEnvironment).not.toHaveProperty(
          'OPENAI_API_KEY'
        )
      }
      for (const name of Object.keys(inheritedProviderCredentials)) {
        const selectedCredential =
          authentication === 'api-key' ? 'OPENAI_API_KEY' : undefined
        if (name !== selectedCredential) {
          expect(launchedEnvironment).not.toHaveProperty(name)
        }
      }
    }
  )

  it('turns a strict upstream error envelope into a failed run', async () => {
    const distribution = await createDistribution()
    let killed = false
    const launchHost: ContinueHostLauncher = () => ({
      exitCode: null,
      get killed() {
        return killed
      },
      stderr: null,
      once: () => undefined,
      kill: () => {
        killed = true
        return true
      }
    })
    let stateRequests = 0
    vi.stubGlobal(
      'fetch',
      vi.fn(async (input: string | URL | Request) => {
        if (String(input).endsWith('/state')) {
          stateRequests += 1
          return Response.json({
            session: {
              history:
                stateRequests === 1
                  ? []
                  : [
                      {
                        message: {
                          role: 'assistant',
                          content: 'Partial response'
                        },
                        toolCallStates: [
                          {
                            toolCallId: 'call-1',
                            toolCall: {
                              function: { name: 'Bash' }
                            },
                            status: 'errored',
                            output: [
                              {
                                content:
                                  'PowerShell 原始错误：路径不存在 ���'
                              }
                            ]
                          }
                        ]
                      },
                      {
                        message: {
                          role: 'system',
                          content:
                            'Error: {"error":{"message":"Request not allowed"}}'
                        }
                      }
                    ]
            },
            isProcessing: false,
            messageQueueLength: 0,
            pendingPermission: null
          })
        }
        return Response.json({})
      })
    )
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: '',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash],
      launchHost,
      modelProfile: {
        id: '00000000-0000-4000-8000-000000000013',
        name: 'Local model',
        baseUrl: 'http://127.0.0.1:11434/v1',
        modelName: 'local-model',
        protocol: 'openai-chat-completions',
        authentication: 'none'
      }
    })

    const run = adapter.run(
      'hello',
      new AbortController().signal,
      async () => 'deny'
    )
    await expect(
      run
    ).rejects.toMatchObject({
      message: 'Continue 模型请求失败：Request not allowed',
      tools: [
        {
          callId: 'call-1',
          name: 'Bash',
          state: 'failed',
          error:
            'PowerShell 原始错误：路径不存在 ���'
        }
      ]
    })
    expect(killed).toBe(true)
  })

  it('returns audit metadata for auto-approved agent tools', async () => {
    const distribution = await createDistribution()
    let launchArgs: string[] = []
    const permissionBodies: unknown[] = []
    const streamEvents: unknown[] = []
    const launchHost: ContinueHostLauncher = (
      _entryPath,
      args
    ) => {
      launchArgs = args
      return {
        exitCode: null,
        killed: false,
        stderr: null,
        once: () => undefined,
        kill: () => true
      }
    }
    let stateRequests = 0
    vi.stubGlobal(
      'fetch',
      vi.fn(
        async (
          input: string | URL | Request,
          init?: RequestInit
        ) => {
          const url = String(input)
          if (url.endsWith('/permission')) {
            permissionBodies.push(JSON.parse(String(init?.body)))
            return Response.json({})
          }
          if (url.endsWith('/state')) {
            stateRequests += 1
            if (stateRequests === 1) {
              return Response.json({
                session: { history: [] },
                isProcessing: false,
                messageQueueLength: 0,
                pendingPermission: null
              })
            }
            if (stateRequests === 2) {
              return Response.json({
                session: { history: [] },
                isProcessing: true,
                messageQueueLength: 0,
                pendingPermission: {
                  toolName: 'Bash',
                  toolArgs: { command: 'npm test' },
                  requestId: 'permission-1'
                },
                goodbuddyEvents: [
                  { type: 'text', delta: '先检查命令。' },
                  {
                    type: 'tool',
                    callId: 'call-1',
                    name: 'Bash',
                    state: 'running',
                    input:
                      '{"command":"npm test","token":"secret-token"}'
                  }
                ]
              })
            }
            return Response.json({
              session: {
                history: [
                  {
                    message: {
                      role: 'assistant',
                      content: 'TOOLS_OK'
                    },
                    toolCallStates: [
                      {
                        toolCallId: 'call-1',
                        toolCall: {
                          function: { name: 'Bash' }
                        },
                        status: 'done'
                      }
                    ]
                  }
                ]
              },
              isProcessing: false,
              messageQueueLength: 0,
              pendingPermission: null,
              goodbuddyEvents: [
                {
                  type: 'tool',
                  callId: 'call-1',
                  name: 'Bash',
                  state: 'completed',
                  output:
                    'Tests passed\nAuthorization: Bearer secret-token'
                },
                { type: 'text', delta: 'TOOLS_OK' }
              ]
            })
          }
          return Response.json({})
        }
      )
    )
    const adapter = new ContinueHostAdapter({
      binaryPath: distribution.entryPath,
      configPath: 'C:\\safe\\continue.yaml',
      workspace: process.cwd(),
      cacheRoot: distribution.cacheRoot,
      trustedBundleHashes: [distribution.sourceHash],
      launchHost,
      mode: 'agent'
    })
    const authorize = vi.fn(async () => 'once' as const)

    await expect(
      adapter.run(
        'hello',
        new AbortController().signal,
        authorize,
        {
          onEvent: (event) => {
            streamEvents.push(event)
          }
        }
      )
    ).resolves.toEqual({
      text: 'TOOLS_OK',
      streamedText: true,
      tools: [
        {
          callId: 'call-1',
          name: 'Bash',
          state: 'completed',
          input:
            '{"command":"npm test","token":"secret-token"}',
          output:
            'Tests passed\nAuthorization: Bearer secret-token'
        }
      ]
    })
    expect(streamEvents).toEqual([
      { type: 'text', delta: '先检查命令。' },
      {
        type: 'tool',
        tool: {
          callId: 'call-1',
          name: 'Bash',
          state: 'running',
          input:
            '{"command":"npm test","token":"secret-token"}'
        }
      },
      {
        type: 'tool',
        tool: {
          callId: 'call-1',
          name: 'Bash',
          state: 'completed',
          output:
            'Tests passed\nAuthorization: Bearer secret-token'
        }
      },
      { type: 'text', delta: 'TOOLS_OK' }
    ])
    expect(launchArgs).not.toContain('--readonly')
    expect(authorize).toHaveBeenCalledWith(
      expect.objectContaining({ toolName: 'Bash' })
    )
    expect(permissionBodies).toEqual([
      { requestId: 'permission-1', approved: true }
    ])
  })

  it.each([
    {
      label: 'Chat Completions',
      protocol: 'openai-chat-completions' as const,
      expectedPath: '/v1/chat/completions',
      unexpectedPath: '/v1/responses'
    },
    {
      label: 'Responses',
      protocol: 'openai-responses' as const,
      expectedPath: '/v1/responses',
      unexpectedPath: '/v1/chat/completions'
    }
  ])(
    'routes a custom-base $label profile to its explicit endpoint in Continue 1.5.47',
    async ({
      protocol,
      expectedPath,
      unexpectedPath
    }) => {
      const root = await mkdtemp(
        join(tmpdir(), 'goodbuddy-continue-responses-')
      )
      temporaryDirectories.push(root)
      const requestPaths: string[] = []
      const requestBodies: unknown[] = []
      const server = createServer(async (request, response) => {
        requestPaths.push(request.url ?? '')
        let body = ''
        for await (const chunk of request) {
          body += chunk
        }
        if (body) {
          requestBodies.push(JSON.parse(body))
        }
        response.writeHead(400, {
          'content-type': 'application/json'
        })
        response.end(
          JSON.stringify({
            error: {
              message: 'Intentional local routing probe'
            }
          })
        )
      })
      await new Promise<void>((resolveListen, reject) => {
        server.once('error', reject)
        server.listen(0, '127.0.0.1', () => resolveListen())
      })
      const address = server.address()
      if (!address || typeof address === 'string') {
        throw new Error('Failed to bind local routing probe')
      }
      const adapter = new ContinueHostAdapter({
        binaryPath: join(
          process.cwd(),
          'node_modules',
          '@continuedev',
          'cli',
          'dist',
          'cn.js'
        ),
        configPath: '',
        workspace: root,
        cacheRoot: join(root, 'cache'),
        modelProfile: {
          id: '00000000-0000-4000-8000-000000000014',
          name: 'Local endpoint probe',
          baseUrl: `http://127.0.0.1:${address.port}/v1`,
          modelName: 'probe-model',
          protocol,
          authentication: 'none'
        }
      })
      const controller = new AbortController()
      const timeout = setTimeout(
        () => controller.abort(new Error('Routing probe timed out')),
        20_000
      )
      try {
        await adapter
          .run('Reply with OK', controller.signal, async () => 'deny')
          .catch(() => undefined)
        expect(requestPaths).toContain(expectedPath)
        expect(requestPaths).not.toContain(unexpectedPath)
        if (protocol === 'openai-chat-completions') {
          const chatRequest = requestBodies.find(
            (body): body is { messages: Array<{ role?: unknown }> } =>
              Boolean(
                body &&
                  typeof body === 'object' &&
                  'messages' in body &&
                  Array.isArray(body.messages)
              )
          )
          expect(chatRequest).toBeDefined()
          expect(chatRequest?.messages[0]?.role).toBe('system')
          expect(
            chatRequest?.messages.filter(
              (message) => message.role === 'system'
            )
          ).toHaveLength(1)
        }
      } finally {
        clearTimeout(timeout)
        adapter.dispose()
        await new Promise((resolveWait) =>
          setTimeout(resolveWait, 500)
        )
        await new Promise<void>((resolveClose, reject) => {
          server.close((error) =>
            error ? reject(error) : resolveClose()
          )
        })
      }
    },
    30_000
  )
})
