feat: make the tool-definition default sort a plugin setting
New 'Tool Schemas - Default sort' row in Settings -> Plugins -> dsh-context (ahead of the File Activity one): size, call count, or name, persisted per user through the dsh-context settings namespace. The Context browser reads it as the mount-time default for its tools toolbar; in-toolbar toggling stays session-local and never writes back. The default default is now call count. Degradation mirrors the existing preferences: the host schema is loose (a stale persisted value falls back to the default), the client drops values outside the vocabulary, and a host without the settings surface (or a section missing the field) keeps the schema default, so the tools list stays fully functional either way.
This commit is contained in:
1 parent
4dded7add5
commit
b6020146df
15 files changed
+169
-44
No files matched your search
@@ -6,6 +6,7 @@ import { CATS, CAT_COLOR, partsOf } from '../categories'
|
||||
import { dnaOf } from '../dna'
|
||||
import type { DnaItem } from '../dna'
|
||||
import type { ContentFetcher, ConversationNodeLike, HeaderFetcher } from '../services'
|
||||
import type { ContextSettings, DefaultToolSort } from '../settings'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
import { blockSummaryOf, callSummaryOf, parseCallArgs } from '../callSummary'
|
||||
import type { DetailState } from '../timelineSource'
|
||||
@@ -581,6 +582,7 @@ const DNA_MIN_BAND = 0.35
|
||||
export function makeContextBrowser(
|
||||
kit: ViewKit,
|
||||
StackedBar: (props: StackedBarProps) => ReactElement,
|
||||
settings: ContextSettings,
|
||||
): (props: ContextBrowserProps) => ReactElement {
|
||||
const { t, fmt, fmtTime, catLabel } = kit
|
||||
const DetailNote = makeDetailNote(kit)
|
||||
@@ -602,7 +604,9 @@ export function makeContextBrowser(
|
||||
// it, while step picks (setOpenCat(null) below) keep it so the same lens
|
||||
// compares epochs.
|
||||
const [rowQuery, setRowQuery] = useState('')
|
||||
const [toolSort, setToolSort] = useState<'size' | 'count' | 'name'>('size')
|
||||
// Mount-time default from the plugin settings card; in-toolbar toggling
|
||||
// stays mount-local and never writes back.
|
||||
const [toolSort, setToolSort] = useState<DefaultToolSort>(() => settings.defaultToolSort())
|
||||
// DNA mode: the composition bar redraws as ONE band per context item in prompt order (dna.ts), hovered/clicked per item.
|
||||
const [dna, setDna] = useState(false)
|
||||
const [dnaKey, setDnaKey] = useState<string | null>(null)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { measureDock } from '../dockMeasure'
|
||||
import { headlineOf } from '../headline'
|
||||
import { modalStoreOf, takePendingConsume } from '../modalStore'
|
||||
import type { ClientCtx, SessionStandardProps, SessionsFace } from '../services'
|
||||
import type { ContextSettings } from '../settings'
|
||||
import { contextBreakdownOf, contextPressureOf, conversationNodesOf, headersOf, imageLoaderOf, projectionOf } from '../services'
|
||||
import { makeContentFetcher, makeHeaderFetcher, useHistoryFace } from '../historyPage'
|
||||
import { useTimelineSource } from '../timelineSource'
|
||||
@@ -24,12 +25,16 @@ export interface ContextModalProps extends SessionStandardProps {
|
||||
useContextModal?: (sel: (open: boolean) => boolean) => boolean
|
||||
}
|
||||
|
||||
export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextModalProps) => ReactElement | null {
|
||||
export function makeContextModal(
|
||||
ctx: ClientCtx,
|
||||
kit: ViewKit,
|
||||
settings: ContextSettings,
|
||||
): (props: ContextModalProps) => ReactElement | null {
|
||||
const { t } = kit
|
||||
const StackedBar = makeStackedBar(kit)
|
||||
const Legend = makeLegend(kit)
|
||||
const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend)
|
||||
const ContextBrowser = makeContextBrowser(kit, StackedBar)
|
||||
const ContextBrowser = makeContextBrowser(kit, StackedBar, settings)
|
||||
const ErrorBoundary = makeErrorBoundary(t)
|
||||
|
||||
function ContextModalBody(props: ContextModalProps): ReactElement | null {
|
||||
|
||||
@@ -65,7 +65,7 @@ export function makeContextView(
|
||||
const PluginInfo = makePluginInfo(kit)
|
||||
const UpgradeGate = makeUpgradeGate(kit)
|
||||
const DetailNote = makeDetailNote(kit)
|
||||
const ContextBrowser = makeContextBrowser(kit, StackedBar)
|
||||
const ContextBrowser = makeContextBrowser(kit, StackedBar, settings)
|
||||
const AgentGraph = makeAgentGraph(ctx, kit)
|
||||
const ErrorBoundary = makeErrorBoundary(t)
|
||||
|
||||
|
||||
@@ -128,6 +128,17 @@ export function makeSettingsCard(kit: ViewKit): (props: SettingsCardProps) => Re
|
||||
]}
|
||||
onPick={(id) => { props.set?.('defaultTrendMode', id) }}
|
||||
/>
|
||||
<PrefRow
|
||||
label={t('settings.toolSort')}
|
||||
value={state.toolSort}
|
||||
disabled={disabled}
|
||||
options={[
|
||||
{ id: 'size', label: t('tool.sort.size') },
|
||||
{ id: 'count', label: t('tool.sort.count') },
|
||||
{ id: 'name', label: t('tool.sort.name') },
|
||||
]}
|
||||
onPick={(id) => { props.set?.('defaultToolSort', id) }}
|
||||
/>
|
||||
<PrefRow
|
||||
label={t('settings.fileSort')}
|
||||
value={state.fileSort}
|
||||
|
||||
+4
-2
@@ -57,6 +57,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
'placement.tab': '标签页', 'placement.sidebar': '侧边栏', 'placement.all': '全部',
|
||||
'settings.gran': '趋势图 · 默认粒度',
|
||||
'settings.mode': '趋势图 · 默认展示方式',
|
||||
'settings.toolSort': '工具定义 · 默认排序',
|
||||
'settings.fileSort': '文件活动 · 默认排序',
|
||||
'settings.expand': '展开', 'settings.collapse': '收起',
|
||||
'settings.readOnly': '当前环境的设置为只读',
|
||||
@@ -205,7 +206,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
'tool.jsonHide': '收起',
|
||||
'tool.search': '按名称、描述或参数过滤…',
|
||||
'tool.sort.size': '按大小', 'tool.sort.count': '按次数', 'tool.sort.name': '按名称',
|
||||
'tool.sortTip': '工具排序:按 token 大小(默认)、当前步骤中的调用次数或名称',
|
||||
'tool.sortTip': '工具排序:按当前步骤中的调用次数(默认)、token 大小或名称',
|
||||
'tool.hitsTip': '当前展示步骤的上下文中,该工具被调用并返回结果的次数',
|
||||
'tool.noMatch': '没有匹配当前过滤条件的工具',
|
||||
'rich.raw': '原文',
|
||||
@@ -289,6 +290,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
'placement.tab': 'Tab', 'placement.sidebar': 'Sidebar', 'placement.all': 'All',
|
||||
'settings.gran': 'Trend Chart · Default granularity',
|
||||
'settings.mode': 'Trend Chart · Default display',
|
||||
'settings.toolSort': 'Tool Schemas · Default sort',
|
||||
'settings.fileSort': 'File Activity · Default sort',
|
||||
'settings.expand': 'Expand', 'settings.collapse': 'Collapse',
|
||||
'settings.readOnly': 'Settings are read-only in this environment',
|
||||
@@ -437,7 +439,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
'tool.jsonHide': 'Collapse',
|
||||
'tool.search': 'Filter by name, description, or parameters…',
|
||||
'tool.sort.size': 'By size', 'tool.sort.count': 'By calls', 'tool.sort.name': 'By name',
|
||||
'tool.sortTip': 'Sort tools by token size (default), call count in the shown step, or name',
|
||||
'tool.sortTip': 'Sort tools by call count in the shown step (default), token size, or name',
|
||||
'tool.hitsTip': 'Times this tool was called and answered within the shown step’s context',
|
||||
'tool.noMatch': 'No tools match the current filter',
|
||||
'rich.raw': 'Raw',
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ function apply(ctx: ClientCtx): void {
|
||||
// the trigger source). The modal itself renders from the input overlay
|
||||
// slot, opened per session through the hooks-compartment store.
|
||||
registerContextCommand(ctx, kit)
|
||||
const ContextModal = makeContextModal(ctx, kit)
|
||||
const ContextModal = makeContextModal(ctx, kit, settings)
|
||||
ctx.slots.inject('conversation.input.overlay', () => {
|
||||
return ctx.slots.register(
|
||||
{ name: 'conversation.input.overlay', id: 'context-modal', order: 10, locale: NS,
|
||||
|
||||
+11
-4
@@ -12,11 +12,11 @@
|
||||
* members keeps the dependency graph honest.
|
||||
*/
|
||||
|
||||
import type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultTrendMode, SettingsField } from '../shared/types'
|
||||
import type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultToolSort, DefaultTrendMode, SettingsField } from '../shared/types'
|
||||
|
||||
// The preference vocabulary is declared once in shared/types.ts; re-exported
|
||||
// here so client-side consumers keep their canonical import path.
|
||||
export type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultTrendMode, SettingsField } from '../shared/types'
|
||||
export type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultToolSort, DefaultTrendMode, SettingsField } from '../shared/types'
|
||||
|
||||
/** The bound settings scope (ctx.settingsScope.bind result), as consumed. */
|
||||
export interface SettingsScopeLike {
|
||||
@@ -37,6 +37,7 @@ export interface SettingsState {
|
||||
placement: DefaultPlacement
|
||||
granularity: DefaultGranularity
|
||||
mode: DefaultTrendMode
|
||||
toolSort: DefaultToolSort
|
||||
fileSort: DefaultFileSort
|
||||
writable: boolean
|
||||
}
|
||||
@@ -47,6 +48,7 @@ export interface ContextSettings {
|
||||
defaultPlacement(): DefaultPlacement
|
||||
defaultGranularity(): DefaultGranularity
|
||||
defaultTrendMode(): DefaultTrendMode
|
||||
defaultToolSort(): DefaultToolSort
|
||||
defaultFileSort(): DefaultFileSort
|
||||
attach(scope: SettingsScopeLike): () => void
|
||||
/** Persist one preference choice (local echo, then the fenced scope write). */
|
||||
@@ -57,6 +59,7 @@ type Prefs = {
|
||||
placement?: DefaultPlacement
|
||||
granularity?: DefaultGranularity
|
||||
mode?: DefaultTrendMode
|
||||
toolSort?: DefaultToolSort
|
||||
fileSort?: DefaultFileSort
|
||||
}
|
||||
|
||||
@@ -67,17 +70,19 @@ function prefsOf(value: unknown): Prefs {
|
||||
...(v.defaultPlacement === 'all' || v.defaultPlacement === 'tab' || v.defaultPlacement === 'sidebar' ? { placement: v.defaultPlacement } : {}),
|
||||
...(v.defaultGranularity === 'step' || v.defaultGranularity === 'turn' ? { granularity: v.defaultGranularity } : {}),
|
||||
...(v.defaultTrendMode === 'total' || v.defaultTrendMode === 'delta' ? { mode: v.defaultTrendMode } : {}),
|
||||
...(v.defaultToolSort === 'size' || v.defaultToolSort === 'count' || v.defaultToolSort === 'name' ? { toolSort: v.defaultToolSort } : {}),
|
||||
...(v.defaultFileSort === 'count' || v.defaultFileSort === 'latest' || v.defaultFileSort === 'path' ? { fileSort: v.defaultFileSort } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function createContextSettings(): ContextSettings {
|
||||
let state: SettingsState = { status: 'loading', placement: 'all', granularity: 'step', mode: 'total', fileSort: 'count', writable: false }
|
||||
let state: SettingsState = { status: 'loading', placement: 'all', granularity: 'step', mode: 'total', toolSort: 'count', fileSort: 'count', writable: false }
|
||||
let scope: SettingsScopeLike | undefined
|
||||
const listeners = new Set<() => void>()
|
||||
const publish = (next: SettingsState): void => {
|
||||
if (next.status === state.status && next.placement === state.placement && next.granularity === state.granularity
|
||||
&& next.mode === state.mode && next.fileSort === state.fileSort && next.writable === state.writable) return
|
||||
&& next.mode === state.mode && next.toolSort === state.toolSort && next.fileSort === state.fileSort
|
||||
&& next.writable === state.writable) return
|
||||
state = next
|
||||
for (const listener of listeners) listener()
|
||||
}
|
||||
@@ -98,6 +103,7 @@ export function createContextSettings(): ContextSettings {
|
||||
placement: prefs.placement ?? (rawPlacement === undefined ? state.placement : 'all'),
|
||||
granularity: prefs.granularity ?? state.granularity,
|
||||
mode: prefs.mode ?? state.mode,
|
||||
toolSort: prefs.toolSort ?? state.toolSort,
|
||||
fileSort: prefs.fileSort ?? state.fileSort,
|
||||
writable: snap.writable,
|
||||
})
|
||||
@@ -114,6 +120,7 @@ export function createContextSettings(): ContextSettings {
|
||||
defaultPlacement: () => state.placement,
|
||||
defaultGranularity: () => state.granularity,
|
||||
defaultTrendMode: () => state.mode,
|
||||
defaultToolSort: () => state.toolSort,
|
||||
defaultFileSort: () => state.fileSort,
|
||||
attach(bound) {
|
||||
scope = bound
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SETTINGS_NAMESPACE = 'dsh-context'
|
||||
|
||||
// The preference vocabulary is declared once in shared/types.ts; re-exported
|
||||
// here so host-side consumers keep their canonical import path.
|
||||
export type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultTrendMode, PluginSettings } from '../shared/types'
|
||||
export type { DefaultFileSort, DefaultGranularity, DefaultPlacement, DefaultToolSort, DefaultTrendMode, PluginSettings } from '../shared/types'
|
||||
|
||||
/** Section schema: also the wire envelope the browser scope validates against. */
|
||||
export const SettingsSchema: z<PluginSettings> = z.object({
|
||||
@@ -31,6 +31,7 @@ export const SettingsSchema: z<PluginSettings> = z.object({
|
||||
defaultPlacement: z.union(['all', 'tab', 'sidebar']).default('all').loose(),
|
||||
defaultGranularity: z.union(['step', 'turn']).default('step'),
|
||||
defaultTrendMode: z.union(['total', 'delta']).default('total').loose(),
|
||||
defaultToolSort: z.union(['size', 'count', 'name']).default('count').loose(),
|
||||
defaultFileSort: z.union(['count', 'latest', 'path']).default('count').loose(),
|
||||
})
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ export type DefaultTrendMode = 'total' | 'delta'
|
||||
/** File Activity row order: most operations first, most-recently-touched first, or path ascending. */
|
||||
export type DefaultFileSort = 'count' | 'latest' | 'path'
|
||||
|
||||
/** Tool-definition row order: largest schema first, most call hits first, or name ascending. */
|
||||
export type DefaultToolSort = 'size' | 'count' | 'name'
|
||||
|
||||
/** Where the Context view is offered: the conversation tab, the right Sidebar, or both. */
|
||||
export type DefaultPlacement = 'all' | 'tab' | 'sidebar'
|
||||
|
||||
@@ -110,6 +113,7 @@ export interface PluginSettings {
|
||||
defaultPlacement: DefaultPlacement
|
||||
defaultGranularity: DefaultGranularity
|
||||
defaultTrendMode: DefaultTrendMode
|
||||
defaultToolSort: DefaultToolSort
|
||||
defaultFileSort: DefaultFileSort
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import { makeStackedBar } from '../../../src/client/components/stackedBar'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { UNKNOWN_TOOL_SOURCE, type ContextHeaders, type ContextTimeline, type HeaderEpochContent, type RequestRecord, type SurfaceNode } from '../../../src/shared/types'
|
||||
import { headersOf, type ConversationNodeLike, type ImageLoader } from '../../../src/client/services'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
import { click, flush, hover, makeKit, mount, query, queryAll, text, unhover, type Mounted } from '../helpers/kit'
|
||||
|
||||
const kit = makeKit()
|
||||
const Browser = makeContextBrowser(kit, makeStackedBar(kit))
|
||||
const settings = createContextSettings()
|
||||
const Browser = makeContextBrowser(kit, makeStackedBar(kit), settings)
|
||||
|
||||
// Category row order is CATS: system, tools, user, inject, assistant, tool.
|
||||
const ROW = { system: 0, tools: 1, user: 2, inject: 3, assistant: 4, tool: 5 } as const
|
||||
@@ -642,6 +644,7 @@ describe('ContextBrowser tool schemas', () => {
|
||||
const lazyProps = () => props({ data, headers, fetchHeader: () => Promise.resolve(content) })
|
||||
|
||||
test('rows rank by token price; the schema narrowing matrix renders', async () => {
|
||||
settings.set('defaultToolSort', 'size')
|
||||
const m = await mount(h(Browser, lazyProps()))
|
||||
await click(catRow(m, 'tools'))
|
||||
await flush()
|
||||
@@ -692,9 +695,11 @@ describe('ContextBrowser tool schemas', () => {
|
||||
await click(elemRows(m)[0])
|
||||
assert.equal(queryAll(m.container, '.lc-br-content').length, 0)
|
||||
await m.unmount()
|
||||
settings.set('defaultToolSort', 'count')
|
||||
})
|
||||
|
||||
test('a text filter and the size/name sort narrow and re-rank the rows', async () => {
|
||||
settings.set('defaultToolSort', 'size')
|
||||
const m = await mount(h(Browser, lazyProps()))
|
||||
await click(catRow(m, 'tools'))
|
||||
await flush()
|
||||
@@ -735,11 +740,13 @@ describe('ContextBrowser tool schemas', () => {
|
||||
await click(sortBtns[0])
|
||||
assert.deepEqual(names(), ['mega', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'theta', 'rho', 'omega'])
|
||||
await m.unmount()
|
||||
settings.set('defaultToolSort', 'count')
|
||||
})
|
||||
|
||||
test('count sort ranks by call hits on the shown surface; collapsed rows carry the tally', async () => {
|
||||
// Sizes anti-correlate with hits so the count ranking is visibly its own
|
||||
// order; delta ties gamma's tally and wins the name tie-break.
|
||||
settings.set('defaultToolSort', 'size')
|
||||
const hitHeaders: ContextHeaders = { headers: [{ seq: 1, time: 1, systemTokens: 3, tools: [
|
||||
{ name: 'alpha', tokens: 100 },
|
||||
{ name: 'beta', tokens: 10 },
|
||||
@@ -782,6 +789,49 @@ describe('ContextBrowser tool schemas', () => {
|
||||
assert.deepEqual(names(), ['beta', 'alpha', 'delta', 'gamma'])
|
||||
assert.deepEqual(hits(), ['×1', '×0', '×0', '×0'])
|
||||
await m.unmount()
|
||||
settings.set('defaultToolSort', 'count')
|
||||
})
|
||||
|
||||
test('the mount-time default tool sort comes from the plugin settings', async () => {
|
||||
const pair: ContextHeaders = { headers: [{ seq: 1, time: 1, systemTokens: 3, tools: [
|
||||
{ name: 'zzz', tokens: 1 },
|
||||
{ name: 'aaa', tokens: 100 },
|
||||
] }] }
|
||||
const data = tl({
|
||||
current: { system: 10, tools: 101, user: 0, inject: 0, assistant: 0, tool: 10, total: 121 },
|
||||
nodes: [node({ seq: 2, cat: 'tool', tool: 'zzz', tokens: 10 })],
|
||||
})
|
||||
const names = (m: Mounted) => elemRows(m).map(r => text(query(r, '.lc-br-preview')))
|
||||
const sortBtns = (container: ParentNode) => queryAll(container, '.lc-br-toolctl .lc-gran-btn')
|
||||
const mountWith = async (sort: string) => {
|
||||
settings.set('defaultToolSort', sort)
|
||||
const m = await mount(h(Browser, props({ data, headers: pair, fetchHeader: () => Promise.resolve({ tools: [] }) })))
|
||||
await click(catRow(m, 'tools'))
|
||||
return m
|
||||
}
|
||||
|
||||
// The schema default: most call hits first (zzz ×1 over aaa ×0).
|
||||
let m = await mountWith('count')
|
||||
assert.ok(sortBtns(m.container)[1].className.includes('lc-gran-on'))
|
||||
assert.deepEqual(names(m), ['zzz', 'aaa'])
|
||||
await m.unmount()
|
||||
|
||||
m = await mountWith('size')
|
||||
assert.ok(sortBtns(m.container)[0].className.includes('lc-gran-on'))
|
||||
assert.deepEqual(names(m), ['aaa', 'zzz'])
|
||||
await m.unmount()
|
||||
|
||||
m = await mountWith('name')
|
||||
assert.ok(sortBtns(m.container)[2].className.includes('lc-gran-on'))
|
||||
assert.deepEqual(names(m), ['aaa', 'zzz'])
|
||||
await m.unmount()
|
||||
|
||||
// In-toolbar toggling never writes the preference back.
|
||||
m = await mountWith('count')
|
||||
await click(sortBtns(m.container)[0])
|
||||
assert.equal(settings.store.getSnapshot().toolSort, 'count')
|
||||
await m.unmount()
|
||||
settings.set('defaultToolSort', 'count')
|
||||
})
|
||||
|
||||
test('before the epoch content loads, the filter scans names and plugins only', async () => {
|
||||
@@ -902,6 +952,7 @@ describe('ContextBrowser tool schemas', () => {
|
||||
})
|
||||
|
||||
test('tool rows tag the registering plugin when attribution exists', async () => {
|
||||
settings.set('defaultToolSort', 'size')
|
||||
const attributed: ContextHeaders = {
|
||||
headers: [{ seq: 1, time: 1, systemTokens: 3, tools: [
|
||||
{ name: 'write', tokens: 5, plugin: '@deepseek-ai/dsh-tool-fs' },
|
||||
@@ -934,6 +985,7 @@ describe('ContextBrowser tool schemas', () => {
|
||||
assert.ok(chip.parentElement!.classList.contains('lc-br-elem-row'), 'plugin chip is a single frame, a direct row child')
|
||||
assert.equal(queryAll(chip, '.lc-br-tag').length, 0, 'no nested tag wrapper')
|
||||
await m.unmount()
|
||||
settings.set('defaultToolSort', 'count')
|
||||
})
|
||||
|
||||
test('a single-tool category opens its schema row with the category; multi stays collapsed', async () => {
|
||||
|
||||
@@ -9,12 +9,14 @@ import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { makeContextModal } from '../../../src/client/components/contextModal'
|
||||
import { modalStoreOf, setPendingConsume, takePendingConsume } from '../../../src/client/modalStore'
|
||||
import { watchHistoryFaces } from '../../../src/client/historyPage'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
import type { ContextTimeline } from '../../../src/shared/types'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { TestClientCtx, TestSessions, asClientCtx } from '../helpers/harness'
|
||||
import { click, flush, hover, keydown, makeKit, mount, query, queryAll, text, unhover } from '../helpers/kit'
|
||||
|
||||
const kit = makeKit()
|
||||
const settings = createContextSettings()
|
||||
|
||||
|
||||
function timeline(over: Record<string, unknown> = {}): ContextTimeline {
|
||||
@@ -46,7 +48,7 @@ afterEach(() => {
|
||||
describe('ContextModal', () => {
|
||||
test('renders null without a useContextModal hook or while closed', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
|
||||
const m1 = await mount(h(ContextModal, { sessionId: 'sm-none', useProjection: () => timeline() }))
|
||||
assert.equal(m1.container.childElementCount, 0)
|
||||
@@ -63,7 +65,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('open with no projection hook (or an empty one) shows the loading view', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
|
||||
const m1 = await mount(h(ContextModal, { sessionId: 'sm-load1', useContextModal: OPEN }))
|
||||
assert.ok(text(m1.container).includes(DICT_EN.loading))
|
||||
@@ -81,7 +83,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('a corrupt timeline projection is sanitized and still renders the composition', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const m = await mount(h(ContextModal, {
|
||||
sessionId: 'sm-garbage',
|
||||
useContextModal: OPEN,
|
||||
@@ -97,7 +99,7 @@ describe('ContextModal', () => {
|
||||
test('full render: subtitle variants and hover link through the composed browser', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const projections: Record<string, unknown> = {
|
||||
contextTimeline: timeline({
|
||||
model: 'deepseek-v4-flash',
|
||||
@@ -129,7 +131,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('subtitle degrades to the model alone, then to nothing', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
|
||||
const m1 = await mount(h(ContextModal, {
|
||||
sessionId: 'sm-sub1',
|
||||
@@ -153,7 +155,7 @@ describe('ContextModal', () => {
|
||||
test('backdrop click without a sessionId is a no-op', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const m = await mount(h(ContextModal, { useContextModal: OPEN, useProjection: () => undefined }))
|
||||
await click(query(m.container, '.lc-modal-backdrop'))
|
||||
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
|
||||
@@ -164,7 +166,7 @@ describe('ContextModal', () => {
|
||||
test('clicking the card itself does not close (stopPropagation)', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const m = await mount(h(ContextModal, { sessionId: 'sm-card', useContextModal: OPEN, useProjection: () => undefined }))
|
||||
await click(query(m.container, '.lc-modal-card'))
|
||||
assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null)
|
||||
@@ -175,7 +177,7 @@ describe('ContextModal', () => {
|
||||
test('close flips the store and bails a pending consume through the session scope', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-close'
|
||||
modalStoreOf(sid).set(true)
|
||||
setPendingConsume(sid, { kind: 'bare-token', token: '/context' })
|
||||
@@ -200,7 +202,7 @@ describe('ContextModal', () => {
|
||||
test('close without a pending consume sets the store but bails nothing', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-noguard'
|
||||
modalStoreOf(sid).set(true)
|
||||
const m = await mount(h(ContextModal, {
|
||||
@@ -216,7 +218,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('a pending consume without the sessions service closes quietly', async () => {
|
||||
const ctx = new TestClientCtx()
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-nosvc'
|
||||
modalStoreOf(sid).set(true)
|
||||
setPendingConsume(sid, { kind: 'bare-token', token: '/context' })
|
||||
@@ -240,7 +242,7 @@ describe('ContextModal', () => {
|
||||
},
|
||||
}
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-noscope'
|
||||
modalStoreOf(sid).set(true)
|
||||
setPendingConsume(sid, { kind: 'span', span: { start: 0, end: 8, draftRev: 3 } })
|
||||
@@ -258,7 +260,7 @@ describe('ContextModal', () => {
|
||||
test('the mask docks beside the frame ancestor, follows template rewrites, and disconnects on close', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-dock'
|
||||
// The shell frame: the plugin matches its INLINE grid template, the one
|
||||
// host anchor carried by every supported baseline (dockMeasure.ts).
|
||||
@@ -299,7 +301,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('without a frame ancestor the mask stays full-viewport', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const m = await mount(h(ContextModal, {
|
||||
sessionId: 'sm-noframe',
|
||||
useContextModal: OPEN,
|
||||
@@ -312,7 +314,7 @@ describe('ContextModal', () => {
|
||||
test('Escape closes; other keys do not; the listener is removed on close', async () => {
|
||||
const sessions = new TestSessions()
|
||||
const ctx = new TestClientCtx({ services: { sessions } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-esc'
|
||||
modalStoreOf(sid).set(true)
|
||||
const m = await mount(h(ContextModal, {
|
||||
@@ -338,7 +340,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('closing restores focus to the previously focused element', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-focus'
|
||||
const btn = document.createElement('button')
|
||||
document.body.appendChild(btn)
|
||||
@@ -365,7 +367,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('closing skips the focus restore when the previous element left the document', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-focus-gone'
|
||||
const btn = document.createElement('button')
|
||||
document.body.appendChild(btn)
|
||||
@@ -388,7 +390,7 @@ describe('ContextModal', () => {
|
||||
|
||||
test('no focus bookkeeping when nothing was focused (activeElement not an element)', async () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const sid = 'sm-focus-null'
|
||||
modalStoreOf(sid).set(true)
|
||||
// Environment control (not a plugin fake): jsdom always reports body;
|
||||
@@ -438,7 +440,7 @@ describe('ContextModal — the split generation', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const head = timeline({
|
||||
counts: { turns: 1, steps: 1, injects: 0, compactions: 0, prunes: 0 },
|
||||
last: { seq: 1, total: 21, prompt: 20 },
|
||||
@@ -467,7 +469,7 @@ describe('ContextModal — the split generation', () => {
|
||||
const ctx = new TestClientCtx({ services: { sessions: new TestSessions() } })
|
||||
// Armed before either service exists: the inject stays pending, the race.
|
||||
watchHistoryFaces(asClientCtx(ctx))
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit)
|
||||
const ContextModal = makeContextModal(asClientCtx(ctx), kit, settings)
|
||||
const projections: Record<string, unknown> = {
|
||||
contextTimeline: timeline(),
|
||||
contextHeaders: { headers: [{ seq: 1, time: 0, systemTokens: 5, tools: [] }] },
|
||||
|
||||
@@ -21,7 +21,7 @@ function hookFor(state: SettingsState) {
|
||||
}
|
||||
|
||||
function stateOf(partial: Partial<SettingsState> = {}): SettingsState {
|
||||
return { status: 'ready', placement: 'all', granularity: 'step', mode: 'total', fileSort: 'count', writable: true, ...partial }
|
||||
return { status: 'ready', placement: 'all', granularity: 'step', mode: 'total', toolSort: 'count', fileSort: 'count', writable: true, ...partial }
|
||||
}
|
||||
|
||||
/** Menu items portaled into document.body while a select is open. */
|
||||
@@ -65,7 +65,7 @@ describe('SettingsCard', () => {
|
||||
assert.equal(head.getAttribute('aria-label'), `${DICT_EN['settings.collapse']}: ${DICT_EN['settings.title']}`)
|
||||
assert.ok(card.className.includes('lc-settings-open'))
|
||||
const selects = queryAll(m.container, '.lc-settings-select')
|
||||
assert.equal(selects.length, 4)
|
||||
assert.equal(selects.length, 5)
|
||||
assert.ok(selects.every(s => (s as HTMLButtonElement).disabled))
|
||||
// Loading is not ready: no read-only note.
|
||||
assert.equal(m.container.querySelector('.lc-settings-note'), null)
|
||||
@@ -107,8 +107,10 @@ describe('SettingsCard', () => {
|
||||
// The granularity row follows.
|
||||
assert.ok(text(selects[1]).includes(DICT_EN['gran.step']))
|
||||
assert.ok(text(selects[2]).includes(DICT_EN['gran.total']))
|
||||
assert.ok(text(m.container).includes(DICT_EN['settings.toolSort']))
|
||||
assert.ok(text(selects[3]).includes(DICT_EN['tool.sort.count']))
|
||||
assert.ok(text(m.container).includes(DICT_EN['settings.fileSort']))
|
||||
assert.ok(text(selects[3]).includes(DICT_EN['files.sort.count']))
|
||||
assert.ok(text(selects[4]).includes(DICT_EN['files.sort.count']))
|
||||
|
||||
await click(selects[1])
|
||||
assert.equal(selects[1].getAttribute('aria-expanded'), 'true')
|
||||
@@ -133,8 +135,25 @@ describe('SettingsCard', () => {
|
||||
])
|
||||
assert.equal(document.body.querySelector('[role="menu"]'), null)
|
||||
|
||||
// The file-sort row writes the last field.
|
||||
// The tool-sort row leads the file-sort one.
|
||||
await click(selects[3])
|
||||
const toolItems = menuItems()
|
||||
assert.deepEqual(toolItems.map(i => text(i)), [
|
||||
DICT_EN['tool.sort.size'],
|
||||
DICT_EN['tool.sort.count'],
|
||||
DICT_EN['tool.sort.name'],
|
||||
])
|
||||
await click(toolItems[2]) // 'By name'
|
||||
assert.deepEqual(calls, [
|
||||
['defaultPlacement', 'sidebar'],
|
||||
['defaultGranularity', 'turn'],
|
||||
['defaultTrendMode', 'delta'],
|
||||
['defaultToolSort', 'name'],
|
||||
])
|
||||
assert.equal(document.body.querySelector('[role="menu"]'), null)
|
||||
|
||||
// The file-sort row writes the last field.
|
||||
await click(selects[4])
|
||||
const sortItems = menuItems()
|
||||
assert.deepEqual(sortItems.map(i => text(i)), [
|
||||
DICT_EN['files.sort.count'],
|
||||
@@ -146,6 +165,7 @@ describe('SettingsCard', () => {
|
||||
['defaultPlacement', 'sidebar'],
|
||||
['defaultGranularity', 'turn'],
|
||||
['defaultTrendMode', 'delta'],
|
||||
['defaultToolSort', 'name'],
|
||||
['defaultFileSort', 'path'],
|
||||
])
|
||||
assert.equal(document.body.querySelector('[role="menu"]'), null)
|
||||
@@ -211,7 +231,7 @@ describe('SettingsCard', () => {
|
||||
const card = query(m.container, '.lc-settings-card')
|
||||
assert.ok(card.className.includes('lc-settings-open'))
|
||||
assert.equal(query(m.container, '.lc-settings-head').getAttribute('aria-expanded'), 'true')
|
||||
assert.equal(queryAll(m.container, '.lc-settings-select').length, 4)
|
||||
assert.equal(queryAll(m.container, '.lc-settings-select').length, 5)
|
||||
assert.equal(scrolled.length, 1, 'the card scrolls itself into view')
|
||||
assert.deepEqual(scrolled[0].arg, { block: 'nearest' })
|
||||
assert.equal(scrolled[0].el, card)
|
||||
|
||||
@@ -313,7 +313,7 @@ describe('client entry: settings card slot', () => {
|
||||
assert.ok(query(m.container, '.lc-settings-card'))
|
||||
await click(query(m.container, '.lc-settings-head'))
|
||||
const selects = queryAll(m.container, '.lc-settings-select')
|
||||
assert.equal(selects.length, 4)
|
||||
assert.equal(selects.length, 5)
|
||||
for (const s of selects) assert.ok((s as HTMLButtonElement).disabled)
|
||||
await m.unmount()
|
||||
ctx.dispose()
|
||||
|
||||
@@ -50,12 +50,14 @@ describe('createContextSettings defaults', () => {
|
||||
placement: 'all',
|
||||
granularity: 'step',
|
||||
mode: 'total',
|
||||
toolSort: 'count',
|
||||
fileSort: 'count',
|
||||
writable: false,
|
||||
})
|
||||
assert.equal(s.defaultPlacement(), 'all')
|
||||
assert.equal(s.defaultGranularity(), 'step')
|
||||
assert.equal(s.defaultTrendMode(), 'total')
|
||||
assert.equal(s.defaultToolSort(), 'count')
|
||||
assert.equal(s.defaultFileSort(), 'count')
|
||||
})
|
||||
})
|
||||
@@ -169,6 +171,7 @@ describe('attach', () => {
|
||||
defaultPlacement: 'sidebar',
|
||||
defaultGranularity: 'turn',
|
||||
defaultTrendMode: 'delta',
|
||||
defaultToolSort: 'name',
|
||||
defaultFileSort: 'path',
|
||||
},
|
||||
writable: true,
|
||||
@@ -179,6 +182,7 @@ describe('attach', () => {
|
||||
placement: 'sidebar',
|
||||
granularity: 'turn',
|
||||
mode: 'delta',
|
||||
toolSort: 'name',
|
||||
fileSort: 'path',
|
||||
writable: true,
|
||||
})
|
||||
@@ -207,6 +211,7 @@ describe('attach', () => {
|
||||
placement: 'all',
|
||||
granularity: 'step',
|
||||
mode: 'total',
|
||||
toolSort: 'count',
|
||||
fileSort: 'count',
|
||||
writable: false,
|
||||
})
|
||||
@@ -217,12 +222,13 @@ describe('attach', () => {
|
||||
const s = createContextSettings()
|
||||
s.attach(new TestSettingsScope({
|
||||
status: 'ready',
|
||||
value: { defaultPlacement: 'window', defaultGranularity: 'bogus', defaultTrendMode: 7, defaultFileSort: 'alpha' },
|
||||
value: { defaultPlacement: 'window', defaultGranularity: 'bogus', defaultTrendMode: 7, defaultToolSort: 'alpha', defaultFileSort: 'alpha' },
|
||||
writable: false,
|
||||
}))
|
||||
assert.equal(s.defaultPlacement(), 'all')
|
||||
assert.equal(s.defaultGranularity(), 'step')
|
||||
assert.equal(s.defaultTrendMode(), 'total')
|
||||
assert.equal(s.defaultToolSort(), 'count')
|
||||
assert.equal(s.defaultFileSort(), 'count')
|
||||
})
|
||||
|
||||
@@ -241,22 +247,25 @@ describe('attach', () => {
|
||||
const s = createContextSettings()
|
||||
s.attach(new TestSettingsScope({
|
||||
status: 'ready',
|
||||
value: { defaultPlacement: 'all', defaultGranularity: 'step', defaultTrendMode: 'total', defaultFileSort: 'count' },
|
||||
value: { defaultPlacement: 'all', defaultGranularity: 'step', defaultTrendMode: 'total', defaultToolSort: 'count', defaultFileSort: 'count' },
|
||||
writable: false,
|
||||
}))
|
||||
assert.equal(s.defaultPlacement(), 'all')
|
||||
assert.equal(s.defaultGranularity(), 'step')
|
||||
assert.equal(s.defaultTrendMode(), 'total')
|
||||
assert.equal(s.defaultToolSort(), 'count')
|
||||
assert.equal(s.defaultFileSort(), 'count')
|
||||
})
|
||||
|
||||
test('missing fields keep the current state', () => {
|
||||
const s = createContextSettings()
|
||||
s.set('defaultPlacement', 'tab')
|
||||
s.set('defaultToolSort', 'size')
|
||||
s.attach(new TestSettingsScope({ status: 'ready', value: { defaultFileSort: 'latest' }, writable: false }))
|
||||
assert.equal(s.defaultPlacement(), 'tab', 'the in-session choice survives a section without the field')
|
||||
assert.equal(s.defaultGranularity(), 'step')
|
||||
assert.equal(s.defaultTrendMode(), 'total')
|
||||
assert.equal(s.defaultToolSort(), 'size', 'the in-session tool sort survives a section without the field')
|
||||
assert.equal(s.defaultFileSort(), 'latest')
|
||||
})
|
||||
|
||||
@@ -278,8 +287,11 @@ describe('attach', () => {
|
||||
scope.emit({ status: 'ready', value: { defaultPlacement: 'sidebar' }, writable: true })
|
||||
assert.equal(calls, 4)
|
||||
assert.equal(s.defaultPlacement(), 'sidebar')
|
||||
scope.emit({ status: 'ready', value: { defaultFileSort: 'path' }, writable: false })
|
||||
scope.emit({ status: 'ready', value: { defaultToolSort: 'name' }, writable: true })
|
||||
assert.equal(calls, 5)
|
||||
assert.equal(s.defaultToolSort(), 'name')
|
||||
scope.emit({ status: 'ready', value: { defaultFileSort: 'path' }, writable: false })
|
||||
assert.equal(calls, 6)
|
||||
assert.equal(s.store.getSnapshot().writable, false)
|
||||
})
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('installSettings', () => {
|
||||
defaultPlacement: 'all',
|
||||
defaultGranularity: 'step',
|
||||
defaultTrendMode: 'total',
|
||||
defaultToolSort: 'count',
|
||||
defaultFileSort: 'count',
|
||||
}, 'schema defaults resolve')
|
||||
})
|
||||
@@ -80,6 +81,7 @@ describe('installSettings', () => {
|
||||
defaultPlacement: 'all',
|
||||
defaultGranularity: 'turn',
|
||||
defaultTrendMode: 'total',
|
||||
defaultToolSort: 'count',
|
||||
defaultFileSort: 'count',
|
||||
}, 'the update resolves over the schema defaults')
|
||||
assert.deepEqual(provider.doc['dsh-context'], { defaultGranularity: 'turn' }, 'the provider persisted the section')
|
||||
@@ -89,6 +91,7 @@ describe('installSettings', () => {
|
||||
defaultPlacement: 'sidebar',
|
||||
defaultGranularity: 'turn',
|
||||
defaultTrendMode: 'delta',
|
||||
defaultToolSort: 'count',
|
||||
defaultFileSort: 'path',
|
||||
}, 'every preference field resolves independently')
|
||||
|
||||
@@ -98,25 +101,27 @@ describe('installSettings', () => {
|
||||
)
|
||||
// The loose fields degrade instead of rejecting: a stale file sort or
|
||||
// placement resolves to the default.
|
||||
await ctx.settings.update(ns, { defaultFileSort: 'net', defaultPlacement: 'window' })
|
||||
await ctx.settings.update(ns, { defaultFileSort: 'net', defaultPlacement: 'window', defaultToolSort: 'net' })
|
||||
assert.deepEqual(ctx.settings.get(ns), {
|
||||
defaultPlacement: 'all',
|
||||
defaultGranularity: 'turn',
|
||||
defaultTrendMode: 'delta',
|
||||
defaultToolSort: 'count',
|
||||
defaultFileSort: 'count',
|
||||
}, 'a stale value degrades to the schema default')
|
||||
assert.deepEqual(
|
||||
provider.doc['dsh-context'],
|
||||
{ defaultPlacement: 'window', defaultGranularity: 'turn', defaultTrendMode: 'delta', defaultFileSort: 'net' },
|
||||
{ defaultPlacement: 'window', defaultGranularity: 'turn', defaultTrendMode: 'delta', defaultToolSort: 'net', defaultFileSort: 'net' },
|
||||
'the stale value stays raw in storage and degrades at read',
|
||||
)
|
||||
})
|
||||
|
||||
test('a stale persisted preference degrades to the default (loose)', async () => {
|
||||
const { ctx } = await boot({ 'dsh-context': { defaultPlacement: 'window', defaultTrendMode: 'net', defaultFileSort: 'alpha' } })
|
||||
const { ctx } = await boot({ 'dsh-context': { defaultPlacement: 'window', defaultTrendMode: 'net', defaultToolSort: 'alpha', defaultFileSort: 'alpha' } })
|
||||
const value = ctx.settings.get(ns) as PluginSettings
|
||||
assert.equal(value.defaultPlacement, 'all', 'the stale placement falls back instead of breaking the section')
|
||||
assert.equal(value.defaultTrendMode, 'total', 'the stale value falls back instead of breaking the section')
|
||||
assert.equal(value.defaultToolSort, 'count', 'the stale tool sort falls back instead of breaking the section')
|
||||
assert.equal(value.defaultFileSort, 'count', 'the stale file sort falls back instead of breaking the section')
|
||||
assert.equal(value.defaultGranularity, 'step')
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user