mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(web): serve workspace files from their own origin
A sandbox header bought isolation by taking the document's origin away, and measuring that cost decided against it: the reported artifact throws SecurityError on load, and because an uncaught exception aborts the rest of its <script>, every listener declared after that line — theme toggle, mobile menu, model tabs — never binds. Two of the four artifacts in the reporting user's workspace were dead pages under it, and they still looked right. A second listener on the API's host, answering /f and nothing else, is the same boundary without the amputation: cross-origin to /api (refused by the Origin fence and by CORS), same-origin with itself (localStorage, cookies and fetch all work). Its port is published into the index page; the browser half reads it to address previews, and its absence — the keyless fixture lane — is what makes a file row fall back to the Host opener instead of a dead tab. fileUrl moves from IWorkspaces to ConnectionHandle: the transport owns both the listener that serves the bytes and the port that addresses it.
This commit is contained in:
48
packages/client/test-runtime/src/connection.ts
Normal file
48
packages/client/test-runtime/src/connection.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/** Test-owned connection face: the transport members features read off `ctx.connection`. */
|
||||
import { workspaceFileSegments, workspaceFileUrl } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type { ConnectionHandle, IApiClient, SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
|
||||
/**
|
||||
* Connection test double. Implements the same `ConnectionHandle` face features
|
||||
* receive as `ctx.connection`, so a production face change breaks this double
|
||||
* at compile time. The wire client is not modelled — a feature that needs one
|
||||
* composes its own connection over a fake api client; this double exists for
|
||||
* the transport facts features read synchronously, above all the
|
||||
* workspace-file URL.
|
||||
*/
|
||||
export class TestConnection implements ConnectionHandle {
|
||||
/**
|
||||
* The workspace-file port the host would have published into the page.
|
||||
* Unset — the default, and the keyless fixture lane's real state — makes
|
||||
* {@link TestConnection.fileUrl} answer `undefined`, which is the signal a
|
||||
* caller falls back to the Host opener on.
|
||||
*/
|
||||
filesPort: number | undefined
|
||||
|
||||
/** The wire client; unused by this double's consumers and absent by construction. */
|
||||
readonly api: IApiClient = undefined as unknown as IApiClient
|
||||
|
||||
/**
|
||||
* Stream-loop starter (inert).
|
||||
* @returns a stop handle that does nothing.
|
||||
*/
|
||||
start(): { stop(): void } {
|
||||
return { stop: () => {} }
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace-file URL, deriving exactly as production does so a feature test
|
||||
* sees the real inside/outside-workspace split.
|
||||
* @param sessionId - the Session whose cwd anchors the path.
|
||||
* @param cwd - that Session's working directory.
|
||||
* @param path - the path a tool reported.
|
||||
* @returns the absolute URL on the workspace-file origin, or undefined when
|
||||
* the path leaves the workspace or no port is published.
|
||||
*/
|
||||
fileUrl(sessionId: SessionId, cwd: string | undefined, path: string): string | undefined {
|
||||
if (this.filesPort === undefined) return undefined
|
||||
const segments = workspaceFileSegments(cwd, path)
|
||||
if (segments === undefined) return undefined
|
||||
return `http://localhost:${String(this.filesPort)}${workspaceFileUrl(sessionId, segments)}`
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,13 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { registerDomSnapshotSerializer } from './snapshot.ts'
|
||||
import { TestSessions } from './sessions.ts'
|
||||
import { TestConnection } from './connection.ts'
|
||||
import { TestWorkspaces } from './workspaces.ts'
|
||||
import type { Stabilizer } from './fixtures.ts'
|
||||
|
||||
export { domSnapshotSerializer, registerDomSnapshotSerializer } from './snapshot.ts'
|
||||
export { FixtureSession, TestSessions } from './sessions.ts'
|
||||
export { TestConnection } from './connection.ts'
|
||||
export { TestWorkspaces } from './workspaces.ts'
|
||||
export { conversationSnapshot, workspaceListState } from './fixtures.ts'
|
||||
export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fixtures.ts'
|
||||
@@ -175,6 +177,8 @@ export class SlotTestRuntime {
|
||||
readonly sessions: TestSessions
|
||||
/** Workspaces double (list observable, recorded intent actions). */
|
||||
readonly workspaces: TestWorkspaces
|
||||
/** The transport double features read as `ctx.connection`. */
|
||||
readonly connection: TestConnection
|
||||
|
||||
private readonly stabilizer: Stabilizer = async (fn) => {
|
||||
await act(async () => { await fn() })
|
||||
@@ -195,8 +199,10 @@ export class SlotTestRuntime {
|
||||
this.root = new TestRoot(slots, this.stabilizer)
|
||||
this.sessions = new TestSessions(this.stabilizer, ctx)
|
||||
this.workspaces = new TestWorkspaces(this.stabilizer)
|
||||
this.connection = new TestConnection()
|
||||
ctx.provide('sessions', this.sessions)
|
||||
ctx.provide('workspaces', this.workspaces)
|
||||
ctx.provide('connection', this.connection)
|
||||
// Capturing install: the production renderer does the rendering; the
|
||||
// wrapper only takes the host face for storeOf (no machinery copied).
|
||||
const renderer = createSlotRenderer()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** Test-owned workspaces face: the renderer standard-kit observable plus recorded actions. */
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { workspaceFileSegments, workspaceFileUrl } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type {
|
||||
DirectoryListing, IWorkspaces, SessionId, SnapshotStore, WorkspaceId, WorkspaceListState, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -99,21 +98,6 @@ export class TestWorkspaces implements IWorkspaces {
|
||||
await (this.stubs.get('openPath')?.(path) as Promise<void> | undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace-file URL (recorded). Runs the production path derivation so a
|
||||
* feature test sees the real in/outside-workspace split; stub to force either.
|
||||
* @param sessionId - the session whose cwd anchors the path.
|
||||
* @param cwd - that session's working directory.
|
||||
* @param path - the path a tool reported.
|
||||
* @returns the origin-relative URL, or undefined outside the workspace.
|
||||
*/
|
||||
fileUrl(sessionId: SessionId, cwd: string | undefined, path: string): string | undefined {
|
||||
this.calls.push({ method: 'fileUrl', args: [sessionId, cwd, path] })
|
||||
const stub = this.stubs.get('fileUrl')
|
||||
if (stub !== undefined) return stub(sessionId, cwd, path) as string | undefined
|
||||
const segments = workspaceFileSegments(cwd, path)
|
||||
return segments === undefined ? undefined : workspaceFileUrl(sessionId, segments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory picker (recorded). The default cancels (null); stub to select.
|
||||
|
||||
Reference in New Issue
Block a user