feat!: move to the alpha track — DSH 0.1.2-alpha.x only (v0.18.0-alpha.0)
DSH itself is still on its 0.1.2-alpha line (npm `alpha` dist-tag), so the
plugin mirrors it: v0.18.0-alpha.0 publishes under the npm `alpha`
dist-tag and supports DSH 0.1.2-alpha.x only. Stable-DSH (0.1.0-rc.8 ~
0.1.1-rc.2) users stay on v0.17.1 (npm `latest`). Verified end-to-end
against a real 0.1.2-alpha.2 host: full vitest suite, typecheck,
build/pack, consumer type surface, and the headless mount smoke 14/14
(the prerelease version also passes `dsh plugin add file:`).
- peer floor raised `^0.1.0-rc.8` -> `^0.1.2-alpha.2` (the mount install
no longer warns: the old floor could not semver-match a prerelease
host); dsh.plugin.json engines.dsh `>=0.1.2-alpha.2`.
- Dropped the pre-alpha compatibility layer:
- e2e host RPC collapsed from the dot/slash dual dialect to slash-only
(rpcAttempt; the 404-probe negotiation and its cache are gone), and
parseLaunchUrl now requires the ?token= launch URL;
- MarkdownText labels collapsed to the nested single shape (the cast
became unnecessary against the alpha.2 declarations);
- the defunct `@deepseek-ai/dsh-client-runtime` row is gone from the
chunk externals allowlist (chunk-loader / tsdown / two specs) and
from dsh.client.inject.
- market-manifest guard now allows an exact prerelease version (alpha
cuts are not the market's verified `latest` target); release.yml picks
the npm dist-tag from the version (prerelease -> `alpha`, else
`latest`) so an alpha can never hijack `latest`.
- Docs retargeted: AGENTS.md §2/§3, README/README_EN badges + v0.18.0
-alpha.0 section + install note (`dsh-better-sidebar@alpha`), guide.
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
/**
|
||||
* Unit tests for the e2e lanes' host-transport contracts (tests/e2e/
|
||||
* host-protocol.ts + host.ts), locking in the two DSH web dialects the lanes
|
||||
* must speak:
|
||||
* host-protocol.ts + host.ts), locking in the DSH 0.1.2-alpha web dialect
|
||||
* the lanes speak:
|
||||
*
|
||||
* - 0.1.1-rc.x: bare-origin launch URL, no browser auth, ApiProxy dot
|
||||
* endpoints (`POST /api/workspace.create`, payload = the bare args).
|
||||
* - 0.1.2-alpha.1+ (Remote gateway + one-time-token browser auth):
|
||||
* `/?token=<43 chars>` launch URL (303 → signed cookie), slash endpoints
|
||||
* (`POST /api/workspace/create`, payload must be exactly `{ args }`, the
|
||||
* envelope method equal to the path endpoint; a dot path is no longer
|
||||
* claimed → 404).
|
||||
* - one-time-token browser auth: `/?token=<43 chars>` launch URL
|
||||
* (303 → signed cookie; a token-less URL is a pre-alpha host and must
|
||||
* fail loudly),
|
||||
* - Remote-gateway slash endpoints (`POST /api/workspace/create`, payload
|
||||
* must be exactly `{ args }` keyed by the controller's parameter name,
|
||||
* the envelope method equal to the path endpoint).
|
||||
*
|
||||
* The shapes were transcribed from the deepseek-harness sources at tags
|
||||
* dsh-v0.1.1-rc.2 and dsh-v0.1.2-alpha.1 (packages/client/connection,
|
||||
* packages/api/gateway, packages/bundle/web-app).
|
||||
* The shapes were transcribed from the deepseek-harness sources at tag
|
||||
* dsh-v0.1.2-alpha.1 and re-verified on the npm-published 0.1.2-alpha.2
|
||||
* (packages/client/connection, packages/api/gateway, packages/bundle/web-app).
|
||||
*/
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import type { APIRequestContext } from '@playwright/test'
|
||||
import { parseLaunchUrl, pageUrlWith, rpcAttempts } from './e2e/host-protocol'
|
||||
import { parseLaunchUrl, pageUrlWith, rpcAttempt } from './e2e/host-protocol'
|
||||
|
||||
const BARE_URL = 'http://127.0.0.1:4199'
|
||||
const TOKEN_URL = 'http://127.0.0.1:4199/?token=AbCdEf0123456789_-AbCdEf0123456789_-AbCd'
|
||||
@@ -27,32 +26,24 @@ const TOKEN_URL = 'http://127.0.0.1:4199/?token=AbCdEf0123456789_-AbCdEf01234567
|
||||
let host: typeof import('./e2e/host')
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubEnv('DSH_E2E_URL', BARE_URL)
|
||||
vi.stubEnv('DSH_E2E_URL', TOKEN_URL)
|
||||
host = await import('./e2e/host')
|
||||
})
|
||||
|
||||
describe('host-protocol: launch URL parsing', () => {
|
||||
it('splits a bare-origin launch URL (0.1.1-rc.x hosts)', () => {
|
||||
const launch = parseLaunchUrl(BARE_URL)
|
||||
expect(launch.origin).toBe(BARE_URL)
|
||||
expect(launch.pageUrl).toBe(BARE_URL)
|
||||
expect(launch.token).toBeUndefined()
|
||||
})
|
||||
|
||||
it('extracts the one-time token of an authenticated launch URL (0.1.2-alpha.1+)', () => {
|
||||
it('extracts the one-time token of an authenticated launch URL', () => {
|
||||
const launch = parseLaunchUrl(TOKEN_URL)
|
||||
expect(launch.origin).toBe(BARE_URL)
|
||||
expect(launch.pageUrl).toBe(TOKEN_URL)
|
||||
expect(launch.token).toBe('AbCdEf0123456789_-AbCdEf0123456789_-AbCd')
|
||||
})
|
||||
|
||||
it('rejects a token-less launch URL (a pre-0.1.2-alpha host the plugin no longer supports)', () => {
|
||||
expect(() => parseLaunchUrl(BARE_URL)).toThrow(/no \?token=/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('host-protocol: page URL query merge', () => {
|
||||
it('appends stamps to a bare-origin URL', () => {
|
||||
expect(pageUrlWith(BARE_URL, { 'dsh-desktop-mode': 'advanced' }))
|
||||
.toBe(`${BARE_URL}/?dsh-desktop-mode=advanced`)
|
||||
})
|
||||
|
||||
it('merges stamps into a token URL without breaking the token (never append ?…?…)', () => {
|
||||
const merged = pageUrlWith(TOKEN_URL, { 'dsh-desktop-mode': 'advanced', 'dsh-desktop-platform': 'win32' })
|
||||
const parsed = new URL(merged)
|
||||
@@ -63,45 +54,33 @@ describe('host-protocol: page URL query merge', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('host-protocol: dual-dialect RPC attempts', () => {
|
||||
it('orders the 0.1.1-rc.x dot dialect first (published hosts never pay a probe)', () => {
|
||||
describe('host-protocol: slash RPC attempts', () => {
|
||||
it('targets the slash endpoint with args wrapped by the parameter name', () => {
|
||||
const args = { path: '/tmp/w' }
|
||||
const [dot, slash] = rpcAttempts('workspace.create', args)
|
||||
expect(dot).toEqual({
|
||||
protocol: 'dot',
|
||||
path: '/api/workspace.create',
|
||||
method: 'workspace.create',
|
||||
payload: args,
|
||||
})
|
||||
// The dot payload is the BARE args object (same reference — no wrapper).
|
||||
expect(dot!.payload).toBe(args)
|
||||
expect(slash).toEqual({
|
||||
protocol: 'slash',
|
||||
expect(rpcAttempt('workspace.create', args)).toEqual({
|
||||
path: '/api/workspace/create',
|
||||
method: 'workspace/create',
|
||||
payload: { args: { request: args } },
|
||||
})
|
||||
})
|
||||
|
||||
it('keys slash args by the parameter name — session/list is literally `_request` (verified on a live 0.1.2-alpha.1 host)', () => {
|
||||
it('keys args by the parameter name — session/list is literally `_request` (verified on a live 0.1.2-alpha host)', () => {
|
||||
// typert gateway: session/list declares `_request` and rejects `{}` /
|
||||
// `{request:{}}` — the wrapper table must reproduce the exact shape.
|
||||
expect(rpcAttempts('session.list', {})[1]).toEqual({
|
||||
protocol: 'slash',
|
||||
expect(rpcAttempt('session.list', {})).toEqual({
|
||||
path: '/api/session/list',
|
||||
method: 'session/list',
|
||||
payload: { args: { _request: {} } },
|
||||
})
|
||||
expect(rpcAttempts('session.create', { workspaceId: 'w' })[1]).toEqual({
|
||||
protocol: 'slash',
|
||||
expect(rpcAttempt('session.create', { workspaceId: 'w' })).toEqual({
|
||||
path: '/api/session/create',
|
||||
method: 'session/create',
|
||||
payload: { args: { request: { workspaceId: 'w' } } },
|
||||
})
|
||||
})
|
||||
|
||||
it('fails loudly for methods without a verified slash args key', () => {
|
||||
expect(() => rpcAttempts('settings.describe', {})).toThrow(/no slash-dialect args key/)
|
||||
it('fails loudly for methods without a verified args key', () => {
|
||||
expect(() => rpcAttempt('settings.describe', {})).toThrow(/no args key/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -136,43 +115,16 @@ function stubApi(responses: StubResponse[]): APIRequestContext & { paths: string
|
||||
return api as unknown as APIRequestContext & { paths: string[]; bodies: unknown[] }
|
||||
}
|
||||
|
||||
describe('host glue: hostRpc dialect negotiation', () => {
|
||||
beforeEach(() => {
|
||||
host.resetHostRpcForTests()
|
||||
})
|
||||
|
||||
it('uses the dot dialect directly when the host answers it (0.1.1-rc.x)', async () => {
|
||||
const api = stubApi([{ status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { items: [] } } } }])
|
||||
const first = await host.hostRpc<{ items: unknown[] }>(api, 'session.list', {})
|
||||
expect(first.value.items).toEqual([])
|
||||
// Second call must NOT probe again: the resolved dialect is cached.
|
||||
await host.hostRpc(api, 'session.list', {})
|
||||
expect(api.paths).toEqual(['/api/session.list', '/api/session.list'])
|
||||
})
|
||||
|
||||
it('falls back to the slash dialect on 404 and caches it (0.1.2-alpha.1+)', async () => {
|
||||
const api = stubApi([
|
||||
{ status: 404, body: { error: 'not found' } },
|
||||
{ status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { sessionId: 'session-1' } } } },
|
||||
])
|
||||
describe('host glue: hostRpc', () => {
|
||||
it('posts the Remote-gateway envelope to the slash endpoint and unwraps the result', async () => {
|
||||
const api = stubApi([{ status: 200, body: { type: 'server-response', rpcId: 'x', result: { ok: true, value: { sessionId: 'session-1' } } } }])
|
||||
const seeded = await host.hostRpc<{ sessionId: string }>(api, 'session.create', { workspaceId: 'w' })
|
||||
expect(seeded.value.sessionId).toBe('session-1')
|
||||
expect(api.paths).toEqual(['/api/session.create', '/api/session/create'])
|
||||
// The slash attempt carries the Remote-gateway payload shape: args keyed
|
||||
// by the controller's parameter name (`request` for session/create),
|
||||
// method equal to the path endpoint.
|
||||
const slashEnvelope = api.bodies[1] as { type: string; method: string; payload: Record<string, unknown> }
|
||||
expect(slashEnvelope.type).toBe('client-request')
|
||||
expect(slashEnvelope.method).toBe('session/create')
|
||||
expect(slashEnvelope.payload).toEqual({ args: { request: { workspaceId: 'w' } } })
|
||||
// Cached: the next call goes straight to the slash endpoint.
|
||||
await host.hostRpc(api, 'session.list', {})
|
||||
expect(api.paths[2]).toBe('/api/session/list')
|
||||
})
|
||||
|
||||
it('fails loudly when neither dialect is claimed', async () => {
|
||||
const api = stubApi([{ status: 404, body: {} }])
|
||||
await expect(host.hostRpc(api, 'workspace.create', { path: '/tmp/w' })).rejects.toThrow(/no dialect answered/)
|
||||
expect(api.paths).toEqual(['/api/session/create'])
|
||||
const envelope = api.bodies[0] as { type: string; method: string; payload: Record<string, unknown> }
|
||||
expect(envelope.type).toBe('client-request')
|
||||
expect(envelope.method).toBe('session/create')
|
||||
expect(envelope.payload).toEqual({ args: { request: { workspaceId: 'w' } } })
|
||||
})
|
||||
|
||||
it('surfaces an envelope error instead of returning it', async () => {
|
||||
|
||||
Reference in New Issue
Block a user