Merge remote-tracking branch 'origin/master' into codex/migrate-to-oxlint

This commit is contained in:
Turtle
2026-07-29 14:32:21 +08:00
308 changed files with 10890 additions and 1735 deletions

View File

@@ -92,6 +92,14 @@ export class FixtureSession implements SessionFace {
throw new Error(`test session "${this.sessionId}": cancel is not stubbed — supply it on the fixture's session face`)
}
/**
* Fail-loud stub; supply `command` on the fixture's session face to exercise it.
* @returns never — always throws.
*/
command(): never {
throw new Error(`test session "${this.sessionId}": command is not stubbed — supply it on the fixture's session face`)
}
/**
* Fail-loud stub; supply `loadOlder` on the fixture's session face to exercise it.
* @returns never — always throws.
@@ -183,6 +191,7 @@ export class TestSessions implements ISessions {
id,
displayTitle: fixture.id,
running: false,
waitingApproval: false,
blank: false,
updatedAt: this.records.size + 1,
...fixture.summary,

View File

@@ -1,7 +1,7 @@
/** Test-owned workspaces face: the renderer standard-kit observable plus recorded actions. */
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type {
IWorkspaces, SessionId, SnapshotStore, WorkspaceId, WorkspaceListState, WorkspaceView,
DirectoryListing, IWorkspaces, SessionId, SnapshotStore, WorkspaceId, WorkspaceListState, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
import { workspaceListState } from './fixtures.ts'
import type { Stabilizer } from './fixtures.ts'
@@ -109,6 +109,48 @@ export class TestWorkspaces implements IWorkspaces {
return null
}
/**
* Browse listing (recorded). The default serves an empty home level; stub
* to shape a tree.
* @param path - absolute directory to list; absent lists the home level.
* @returns the level's listing.
*/
async listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing> {
// The signal is recorded and forwarded like the production face passes
// it to the wire, so cancellation integration tests can observe or
// reject on a superseded scan.
this.calls.push({ method: 'listDirectory', args: [path, signal] })
const stub = this.stubs.get('listDirectory')
if (stub !== undefined) return await (stub(path, signal) as Promise<DirectoryListing>)
// The chain runs root-to-target inclusive, per the DirectoryListing
// contract — a bare root crumb would mislabel the level in browsers
// driven by this double.
return {
path: '/home/test',
home: '/home/test',
crumbs: [
{ name: '/', path: '/', hidden: false },
{ name: 'home', path: '/home', hidden: false },
{ name: 'test', path: '/home/test', hidden: false },
],
entries: [],
truncated: false,
}
}
/**
* Browse child creation (recorded). The default joins parent and name.
* @param path - absolute existing parent directory.
* @param name - single path segment.
* @returns the created directory's absolute path.
*/
async createDirectory(path: string, name: string): Promise<string> {
this.calls.push({ method: 'createDirectory', args: [path, name] })
const stub = this.stubs.get('createDirectory')
if (stub !== undefined) return await (stub(path, name) as Promise<string>)
return `${path}/${name}`
}
/**
* Rename a Workspace (recorded). The default echoes a minimal view.
* @param workspaceId - target workspace.

View File

@@ -322,6 +322,32 @@ describe('workspaces', () => {
expect(stub).toHaveBeenCalledOnce()
await runtime.dispose()
})
it('records the browse calls: listDirectory serves an empty home, createDirectory joins, stubs override', async () => {
const runtime = await runtimeWithFrame()
// Defaults: an empty home level and parent/name joining.
await expect(runtime.workspaces.listDirectory()).resolves.toMatchObject({ path: '/home/test', entries: [] })
await expect(runtime.workspaces.listDirectory('/home/test')).resolves.toMatchObject({ path: '/home/test' })
await expect(runtime.workspaces.createDirectory('/home/test', 'fresh')).resolves.toBe('/home/test/fresh')
// The recorded signal seat mirrors the production face (undefined here;
// cancellation tests pass and observe a real one).
expect(runtime.workspaces.calls).toEqual([
{ method: 'listDirectory', args: [undefined, undefined] },
{ method: 'listDirectory', args: ['/home/test', undefined] },
{ method: 'createDirectory', args: ['/home/test', 'fresh'] },
])
// Stubs replace the defaults like every sibling method.
const listing = { path: '/x', home: '/x', crumbs: [], entries: [] }
const listStub = vi.fn(() => Promise.resolve(listing as never))
runtime.workspaces.stub('listDirectory', listStub)
runtime.workspaces.stub('createDirectory', vi.fn(() => Promise.resolve('/x/made' as never)))
const scan = new AbortController()
await expect(runtime.workspaces.listDirectory('/x', scan.signal)).resolves.toBe(listing)
// The stub receives the signal too, like the production face gives the wire.
expect(listStub).toHaveBeenLastCalledWith('/x', scan.signal)
await expect(runtime.workspaces.createDirectory('/x', 'made')).resolves.toBe('/x/made')
await runtime.dispose()
})
})
describe('feature mount and disposal', () => {
@@ -442,6 +468,7 @@ describe('fixture session face', () => {
const bare = runtime.sessions.behavior('s1')
expect(() => bare.prompt()).toThrow(/prompt is not stubbed/)
expect(() => bare.cancel()).toThrow(/cancel is not stubbed/)
expect(() => bare.command()).toThrow(/command is not stubbed/)
expect(() => bare.loadOlder()).toThrow(/loadOlder is not stubbed/)
await runtime.dispose()
})