The web-runtime row provides webApp.getAdvertisedUrl(): the row's configured publicUrl, otherwise the listener origin. harness-dev-context reads it instead of webStartup.publicUrl, so a profile override of publicUrl reaches the model as it already reaches DSH_WEB_URL and the startup URL. The new package no longer injects webServer or webStartup, and the startup flag provider returns to its master behavior.
196 lines
7.5 KiB
TypeScript
196 lines
7.5 KiB
TypeScript
/**
|
|
* The Web command-line provider over a real Loader tree: its ordinary service
|
|
* releases a consumer whose config reads `ctx.webStartup` directly.
|
|
*/
|
|
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
import Include from '@deepseek-ai/cordis-plugin-include'
|
|
import { internals, provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { apply, WEB_STARTUP_SERVICE, type WebStartupValues } from '../src/startup.ts'
|
|
|
|
/** What one fixture boot observed. */
|
|
interface Observed {
|
|
exits: number[]
|
|
out: string
|
|
readerConfig?: unknown
|
|
}
|
|
|
|
const disposers: (() => Promise<void>)[] = []
|
|
|
|
/** Fixture tree roots, removed after their booted tree has been disposed. */
|
|
const tempDirs: string[] = []
|
|
|
|
afterEach(async () => {
|
|
for (const dispose of disposers.splice(0)) await dispose()
|
|
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
internals.stdout = process.stdout
|
|
internals.stderr = process.stderr
|
|
})
|
|
|
|
/**
|
|
* Mount the real provider and a consumer using injection-ordered config.
|
|
* @param args - the invocation's inner arguments.
|
|
* @returns the service value and observed consumer/process effects.
|
|
*/
|
|
async function bootProvider(args: string[]): Promise<{
|
|
values: WebStartupValues | undefined
|
|
observed: Observed
|
|
}> {
|
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-web-startup-'))
|
|
tempDirs.push(dir)
|
|
const observed: Observed = { exits: [], out: '' }
|
|
writeFileSync(join(dir, 'reader.mjs'), `
|
|
export function apply(_ctx, config) { globalThis.__webStartupObserved.readerConfig = config }
|
|
`)
|
|
// Node imports the fixture row outside Vite's source resolver, so delegate
|
|
// to the source-plane plugin already imported by this test.
|
|
writeFileSync(join(dir, 'provider.mjs'), `
|
|
export const name = 'web-startup'
|
|
export const inject = ['cmdlineArgs']
|
|
export const apply = ctx => globalThis.__webStartupApply(ctx)
|
|
`)
|
|
writeFileSync(join(dir, 'cordis.yml'), [
|
|
'- id: reader',
|
|
` name: ${pathToFileURL(join(dir, 'reader.mjs')).href}`,
|
|
` inject: [${WEB_STARTUP_SERVICE}]`,
|
|
' config:',
|
|
" host: !!js ctx.webStartup.host ?? '127.0.0.1'",
|
|
' openBrowser: !!js ctx.webStartup.openBrowser',
|
|
' port: !!js ctx.webStartup.port ?? 3080',
|
|
' publicUrl: !!js ctx.webStartup.publicUrl',
|
|
' trustedHosts: !!js ctx.webStartup.trustedHosts',
|
|
' tls: !!js ctx.webStartup.tls',
|
|
'- id: provider',
|
|
` name: ${pathToFileURL(join(dir, 'provider.mjs')).href}`,
|
|
'',
|
|
].join('\n'))
|
|
const observing = { write: (chunk: string) => { observed.out += chunk; return true } }
|
|
internals.stdout = observing
|
|
internals.stderr = observing
|
|
const globals = globalThis as unknown as {
|
|
__webStartupApply: typeof apply
|
|
__webStartupObserved: Observed
|
|
}
|
|
globals.__webStartupApply = apply
|
|
globals.__webStartupObserved = observed
|
|
|
|
const ctx = new Context()
|
|
await ctx.plugin(Loader)
|
|
ctx.loader.builtins.include = Include
|
|
provideCmdline(ctx, { args, exit: code => void observed.exits.push(code) })
|
|
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(join(dir, 'cordis.yml')).href } })
|
|
await ctx.loader.await()
|
|
disposers.push(async () => { await ctx.fiber.dispose() })
|
|
return {
|
|
values: ctx.get(WEB_STARTUP_SERVICE) as WebStartupValues | undefined,
|
|
observed,
|
|
}
|
|
}
|
|
|
|
describe('web command-line provider', () => {
|
|
it('publishes each flag and releases direct service expressions', async () => {
|
|
const { values, observed } = await bootProvider([
|
|
'--host', '127.0.0.1',
|
|
'--no-open',
|
|
'--port', '8080',
|
|
'--trusted-host', 'lab.internal', 'lab-2.internal',
|
|
'--trusted-host', '10.0.0.9',
|
|
'--tls-cert', 'fullchain.pem',
|
|
'--tls-key', 'private-key.pem',
|
|
])
|
|
expect(values).toEqual({
|
|
host: '127.0.0.1',
|
|
openBrowser: false,
|
|
port: 8080,
|
|
trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'],
|
|
tls: { certFile: 'fullchain.pem', keyFile: 'private-key.pem' },
|
|
})
|
|
expect(observed.readerConfig).toEqual(values)
|
|
expect(observed.exits).toEqual([])
|
|
})
|
|
|
|
// The webserver suite owns the wildcard spellings; the CLI only has to
|
|
// refuse them by address value before any consumer activates.
|
|
it.each(['0.0.0.0', '::ffff:0.0.0.0'])('rejects wildcard --host %s before activating consumers', async (host) => {
|
|
const { values, observed } = await bootProvider(['--host', host])
|
|
expect(observed.out).toContain(`error: --host ${host} is an unspecified (wildcard) address`)
|
|
expect(values).toBeUndefined()
|
|
expect(observed.readerConfig).toBeUndefined()
|
|
expect(observed.exits).toEqual([1])
|
|
})
|
|
|
|
it('leaves deployment values to each consumer when flags omit them', async () => {
|
|
const { values, observed } = await bootProvider([])
|
|
expect(values).toEqual({ openBrowser: true, trustedHosts: [] })
|
|
expect(observed.readerConfig).toEqual({
|
|
host: '127.0.0.1',
|
|
openBrowser: true,
|
|
port: 3080,
|
|
trustedHosts: [],
|
|
})
|
|
})
|
|
|
|
it('prints its own help and leaves the consumer pending', async () => {
|
|
const { values, observed } = await bootProvider(['--help'])
|
|
expect(observed.out).toContain('dsh --profile web')
|
|
expect(observed.out).toContain('--no-open')
|
|
expect(observed.out).toContain('--public-url')
|
|
expect(observed.out).toContain('--trusted-host')
|
|
expect(values).toBeUndefined()
|
|
expect(observed.readerConfig).toBeUndefined()
|
|
expect(observed.exits).toEqual([0])
|
|
})
|
|
|
|
it('rejects a non-numeric port before the consumer activates', async () => {
|
|
const { values, observed } = await bootProvider(['--port', 'abc'])
|
|
expect(observed.out).toContain('--port must be a number')
|
|
expect(values).toBeUndefined()
|
|
expect(observed.readerConfig).toBeUndefined()
|
|
expect(observed.exits).toEqual([1])
|
|
})
|
|
|
|
it('publishes --public-url as advertisement only, leaving the fence to --trusted-host', async () => {
|
|
const { values, observed } = await bootProvider([
|
|
'--public-url', 'https://web.example/ui',
|
|
'--trusted-host', 'lab.internal',
|
|
])
|
|
expect(values).toEqual({
|
|
openBrowser: true,
|
|
publicUrl: 'https://web.example/ui',
|
|
trustedHosts: ['lab.internal'],
|
|
})
|
|
expect(observed.readerConfig).toEqual({
|
|
host: '127.0.0.1',
|
|
openBrowser: true,
|
|
port: 3080,
|
|
publicUrl: 'https://web.example/ui',
|
|
trustedHosts: ['lab.internal'],
|
|
})
|
|
expect(observed.exits).toEqual([])
|
|
})
|
|
|
|
it.each(['--tls-cert', '--tls-key'])('rejects an unpaired %s before activating consumers', async (flag) => {
|
|
const { values, observed } = await bootProvider([flag, 'server.pem'])
|
|
expect(observed.out).toContain('--tls-cert and --tls-key must be supplied together')
|
|
expect(values).toBeUndefined()
|
|
expect(observed.readerConfig).toBeUndefined()
|
|
expect(observed.exits).toEqual([1])
|
|
})
|
|
|
|
it('rejects a malformed --public-url before the consumer activates', async () => {
|
|
// The parser's own suite owns the exhaustive spellings; the provider only
|
|
// has to fail the invocation before any consumer activates.
|
|
const { values, observed } = await bootProvider(['--public-url', '/web/ui'])
|
|
expect(observed.out).toContain('error: --public-url must be an absolute http or https URL of the form http(s)://host[/prefix]')
|
|
expect(values).toBeUndefined()
|
|
expect(observed.readerConfig).toBeUndefined()
|
|
expect(observed.exits).toEqual([1])
|
|
})
|
|
})
|