| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- /**
- * REAL-composition coverage: a test-only cordis.yml booted through the
- * vendored Loader mounts the webserver row plus the adaptive chooser, and the
- * assertions observe the durable outcome — which backend entry the chooser
- * mounted into the Loader store, the capability the seam then serves, and
- * that disposing the chooser removes the mounted entry again (HMR safety),
- * joining the backend's own teardown before the disposer settles.
- */
- import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
- import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { pathToFileURL } from 'node:url'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { Context } from 'cordis'
- import Loader from '@cordisjs/plugin-loader'
- import Include from '@cordisjs/plugin-include'
- import HttpServer from '@deepseek-ai/dsh-host-webserver'
- import type { DirectoryPicker } from '@deepseek-ai/dsh-host-directory-picker'
- import BrowseDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-browse'
- import NativeDirectoryPicker from '@deepseek-ai/dsh-host-directory-picker-native'
- import * as DirectoryPickerAuto from '../src/index.ts'
- const AUTO = '@deepseek-ai/dsh-host-directory-picker-auto'
- const NATIVE = '@deepseek-ai/dsh-host-directory-picker-native'
- const BROWSE = '@deepseek-ai/dsh-host-directory-picker-browse'
- let root: string | undefined
- let fakeBin: string | undefined
- let context: Context | undefined
- afterEach(async () => {
- vi.unstubAllEnvs()
- await context?.fiber.dispose()
- context = undefined
- for (const dir of [root, fakeBin]) {
- // maxRetries absorbs teardown stragglers (e.g. an unawaited fiber's late
- // file handle) that can otherwise race the recursive scan into ENOTEMPTY.
- if (dir !== undefined) await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 })
- }
- root = undefined
- fakeBin = undefined
- })
- /** Write a dist fixture and a two-row cordis.yml (webserver + chooser), then boot it through the real Loader. */
- async function loadComposition(bindHost: '127.0.0.1' | '0.0.0.0'): Promise<{ ctx: Context; configPath: string }> {
- root = await mkdtemp(join(tmpdir(), 'dsh-directory-picker-auto-'))
- const dist = join(root, 'dist')
- mkdirSync(dist)
- const distIndex = join(dist, 'index.html')
- await writeFile(distIndex, '<head></head><body>shell</body>')
- const configPath = join(root, 'cordis.yml')
- await writeFile(configPath, [
- "- name: '@deepseek-ai/dsh-host-webserver'",
- ' config:',
- ` host: '${bindHost}'`,
- ' port: 0',
- ' portConflict: increment',
- ` distIndex: '${distIndex}'`,
- `- name: '${AUTO}'`,
- '',
- ].join('\n'))
- context = new Context()
- context.baseUrl = pathToFileURL(root).href + '/'
- await context.plugin(Loader)
- context.loader.builtins.include = Include
- const modules = new Map<string, unknown>([
- ['@deepseek-ai/dsh-host-webserver', HttpServer],
- [AUTO, DirectoryPickerAuto],
- [NATIVE, NativeDirectoryPicker],
- [BROWSE, BrowseDirectoryPicker],
- ])
- context.loader.internal = {
- version: 'v2',
- async import(specifier: string) {
- if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
- return modules.get(specifier)
- },
- } as unknown as NonNullable<typeof context.loader.internal>
- await context.loader.create({
- name: 'cordis:include',
- config: { path: pathToFileURL(configPath).href },
- })
- await context.loader.await()
- return { ctx: context, configPath }
- }
- /** Entry names currently present in the loader store (root tree plus subtrees). */
- function entryNames(ctx: Context): string[] {
- return [...ctx.loader.entries()].map(entry => entry.options.name)
- }
- /**
- * Force every signal of an attended host on any platform: no SSH launch, a
- * display, and a PATH holding one executable chooser binary so the real
- * probe resolves identically on hosts with and without zenity/kdialog.
- */
- function stubAttendedHost(): void {
- fakeBin = mkdtempSync(join(tmpdir(), 'dsh-picker-bin-'))
- const zenity = join(fakeBin, 'zenity')
- writeFileSync(zenity, '#!/bin/sh\n')
- chmodSync(zenity, 0o755)
- vi.stubEnv('PATH', fakeBin)
- vi.stubEnv('SSH_CONNECTION', '')
- vi.stubEnv('SSH_TTY', '')
- vi.stubEnv('DISPLAY', ':0')
- }
- describe('real Loader composition', () => {
- // The 60s budget covers this file's static imports (webserver plus both
- // backend node halves through tsx), which dominate on cold caches; the
- // Loader itself resolves nothing here — `loader.internal` is a module map.
- it('mounts the native backend for an attended loopback host and unmounts it on disposal', { timeout: 60_000 }, async () => {
- stubAttendedHost()
- const { ctx, configPath } = await loadComposition('127.0.0.1')
- const unloaded = [...ctx.loader.entries()]
- .filter(entry => entry.fiber === undefined && !entry.disabled)
- .map(entry => entry.options.name)
- expect(unloaded).toEqual([])
- expect(entryNames(ctx)).toContain(NATIVE)
- expect(entryNames(ctx)).not.toContain(BROWSE)
- const picker = ctx.get('directoryPicker') as DirectoryPicker
- expect(picker.capability().kind).toBe('native')
- // The mounted row lives in the Loader's in-memory root tree only — the
- // booted config file must never gain the resolved backend row.
- expect(await readFile(configPath, 'utf8')).not.toContain(NATIVE)
- // HMR safety: disposing the chooser's fiber removes the entry it created,
- // and the disposer joins the backend's teardown — the service is gone the
- // moment dispose() settles, with no further loader await.
- const autoEntry = [...ctx.loader.entries()].find(entry => entry.options.name === AUTO)!
- await autoEntry.fiber!.dispose()
- expect(entryNames(ctx)).not.toContain(NATIVE)
- expect(ctx.get('directoryPicker')).toBeUndefined()
- // Self-disposing an include-tree entry persists `disabled: true` (loader
- // behavior, not the chooser's); await that debounced write so it cannot
- // race the temp-dir removal, and pin that the persisted row is the
- // chooser itself — the resolved backend still never reaches the file.
- await expect.poll(async () => await readFile(configPath, 'utf8')).toContain('disabled: true')
- expect(await readFile(configPath, 'utf8')).not.toContain(NATIVE)
- })
- it('mounts the browse backend under an SSH launch', { timeout: 60_000 }, async () => {
- stubAttendedHost()
- vi.stubEnv('SSH_CONNECTION', '10.0.0.2 55 10.0.0.9 22')
- const { ctx } = await loadComposition('127.0.0.1')
- expect(entryNames(ctx)).toContain(BROWSE)
- expect(entryNames(ctx)).not.toContain(NATIVE)
- const picker = ctx.get('directoryPicker') as DirectoryPicker
- expect(picker.capability().kind).toBe('browse')
- })
- it('mounts the browse backend for an all-interfaces bind even on an attended host', { timeout: 60_000 }, async () => {
- stubAttendedHost()
- const { ctx } = await loadComposition('0.0.0.0')
- expect(entryNames(ctx)).toContain(BROWSE)
- expect(entryNames(ctx)).not.toContain(NATIVE)
- })
- it('tolerates the mounted entry being removed by the tree before the chooser unloads', { timeout: 60_000 }, async () => {
- stubAttendedHost()
- const { ctx, configPath } = await loadComposition('127.0.0.1')
- const backendEntry = [...ctx.loader.entries()].find(entry => entry.options.name === NATIVE)!
- await ctx.loader.remove(backendEntry.id)
- const autoEntry = [...ctx.loader.entries()].find(entry => entry.options.name === AUTO)!
- await expect(autoEntry.fiber!.dispose()).resolves.not.toThrow()
- expect(entryNames(ctx)).not.toContain(NATIVE)
- // Same self-dispose persistence as above: let the write land before teardown.
- await expect.poll(async () => await readFile(configPath, 'utf8')).toContain('disabled: true')
- })
- })
|