fix(host): read the balance facts through both settings generations

The 0.1.7 line's SettingsForms service retired the `get(ns)` section
read, so the balance route never resolved the DeepSeek API key on V4+
deployments and the capsule stayed hidden. deepseekSectionOf now folds
over whichever face the running line serves: the registered
`llm-deepseek` section through V3, and on V4+ the Config-form
`describe()` projection, matching the provider's row by its own
apiKeyEnv-bearing section shape and preferring the settings ids the
generations have served it as. Rows fold in isolation, and a describe
that throws is no section — the capsule still arms nowhere it cannot.
The per-baseline `balance` probe pins the settings face and the
provider row's sources against each tag's real sources.
This commit is contained in:
bowenliang123 committed 2026-09-27 02:15:38 +08:00
1 parent 7ce259384f
commit 423ea4f2ac
5 files changed
+277 -13

No files matched your search

+1 -1
View File
@@ -73,7 +73,7 @@ The File Activity card's file-name affordance is an optional-column story: on a
The session jump — a Context Dashboard session card or an Agent network node bringing its session forward — rides the harness's own selection verb (`openSessionVia` in `src/client/services.ts`): `uiWorkspace.openSession`, the verb the built-in sidebar's row click uses on every supported line, with the sessions service's retired `open(id)` spelling kept as the degradation path for a composition without the workspace module. Both faces are re-proved per call and a deployment serving neither (or a hostile face) swallows silently — the panel still closes, the jump just does not land. Issue #90 was exactly the retired-spelling trap: the plugin called `sessions.open` alone, which the 0.1.6 selection refactor removed from the service (the click then did nothing on `0.1.7-rc.2`), so the jump now follows the view owner first and the per-baseline `sessionNav` probe pins both spellings presence-as-declared.
The Context Dashboard's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The capsule appears only on a live, well-formed answer; a deployment whose llm-deepseek provider is not composed (no `llm-deepseek` settings section), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state.
The Context Dashboard's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The provider's settings row is read through whichever face the running line serves: the `get(ns)` registered-section read through V3 (the `llm-deepseek` namespace), the Config-form `describe()` projection on V4+ — there the row is the entry whose served value declares the top-level `apiKeyEnv` credential ref, preferred under the settings ids the generations have served it as (`llm-deepseek` on product profiles, `llm-deepseek-api-key` elsewhere). The capsule appears only on a live, well-formed answer; a deployment whose DeepSeek API-key provider is not composed (no settings row on either face), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state. The `balance` probe pins the settings face and the provider row's sources per baseline.
## What each check means
+58 -12
View File
@@ -5,13 +5,13 @@
* The platform exposes exactly one account endpoint (`GET /user/balance`);
* reaching it needs the API key, which by design never rides to the browser,
* so the HOST reads it and serves the redacted figures. The connection facts
* resolve per request, exactly as llm-deepseek serves its own requests: its
* settings section (`llm-deepseek`, installed only when that provider
* composes) carries the credential ref and the optional endpoint override,
* and the credentials service resolves the ref to the key. Any missing fact
* — the provider absent, no settings service, no key — answers a typed
* `null`, as does a failed or malformed platform read: the capsule renders
* nothing rather than a stale figure.
* resolve per request, exactly as the DeepSeek provider serves its own
* requests: its settings row carries the credential ref and the optional
* endpoint override, read through whichever face the running line serves
* (deepseekSectionOf), and the credentials service resolves the ref to the
* key. Any missing fact — the provider absent, no settings service, no key —
* answers a typed `null`, as does a failed or malformed platform read: the
* capsule renders nothing rather than a stale figure.
*
* The transport is Connection's fetch-route registry (the same authenticated
* `/api` fence host/detail.ts mounts), registered through a deferred inject
@@ -28,7 +28,9 @@ import type { PlatformBalance, PlatformBalanceEntry } from '../shared/types'
/** The plugin's balance route, under the authenticated `/api` fence. */
export const BALANCE_ROUTE = '/api/dsh-context/balance'
/** The settings namespace llm-deepseek installs its connection section under. */
/** The settings id the DeepSeek API-key provider has served its connection
* section under: the V3 registered namespace, kept as the entry id on V4+
* product profiles. */
const DEEPSEEK_SETTINGS_NS = 'llm-deepseek'
/** llm-deepseek's default credential ref (the env-var name its section resolves). */
@@ -40,9 +42,15 @@ const PUBLIC_BASE_URL = 'https://api.deepseek.com'
/** One platform read's whole budget — never worth blocking the route longer. */
const FETCH_TIMEOUT_MS = 10_000
/** The harness `settings` service, as far as the route consumes it. */
/**
* The harness `settings` service, as far as the route consumes it. The face
* is generation-specific: V3 reads a registered section back (`get`), while
* V4+ retired that face and projects every configurable entry instead
* (`describe`) — the route folds over whichever the running line serves.
*/
interface SettingsHostFace {
get?(ns: string): unknown
describe?(): unknown
}
/** The harness `credentials` service, as far as the route consumes it. */
@@ -78,15 +86,53 @@ function amountOf(value: unknown): number | null {
/** The connection facts one read needs, as far as runtime can prove them. */
interface DeepSeekFacts { baseUrl: string; apiKey: string }
/**
* The DeepSeek API-key provider's settings section, read through whichever
* face the running line serves. V3's `get` reads the registered
* `llm-deepseek` section directly; V4+ retired that face, so its `describe()`
* projection is folded instead — the provider's row is the one whose served
* value declares the top-level `apiKeyEnv` credential ref (the volatile shape
* only that provider declares at the section root), preferred under the entry
* ids the generations have served it as (`llm-deepseek` on product profiles,
* `llm-deepseek-api-key` elsewhere). Rows fold in isolation — a hostile row
* drops whole, valid siblings keep serving — and a describe that throws is no
* section at all.
*/
function deepseekSectionOf(ctx: Context): Record<string, unknown> | null {
const settings = ctx.get('settings') as SettingsHostFace | undefined
const section = typeof settings?.get === 'function' ? asRecord(settings.get(DEEPSEEK_SETTINGS_NS)) : null
if (section !== null) return section
if (typeof settings?.describe !== 'function') return null
try {
const rows = settings.describe()
if (!Array.isArray(rows)) return null
let shaped: Record<string, unknown> | null = null
for (const row of rows) {
try {
const record = asRecord(row)
if (record === null) continue
const value = asRecord(record.value)
if (value === null || typeof value.apiKeyEnv !== 'string' || value.apiKeyEnv === '') continue
if (record.ns === DEEPSEEK_SETTINGS_NS || record.ns === 'llm-deepseek-api-key') return value
shaped ??= value
} catch {
continue
}
}
return shaped
} catch {
return null
}
}
/**
* Resolve the DeepSeek connection facts exactly as llm-deepseek serves its
* requests: its settings section for the credential ref and endpoint, the
* requests: its settings row for the credential ref and endpoint, the
* credentials service for the key. `null` whenever any fact is missing —
* the platform is simply not configured for this deployment.
*/
async function resolveFacts(ctx: Context): Promise<DeepSeekFacts | null> {
const settings = ctx.get('settings') as SettingsHostFace | undefined
const section = typeof settings?.get === 'function' ? asRecord(settings.get(DEEPSEEK_SETTINGS_NS)) : null
const section = deepseekSectionOf(ctx)
if (section === null) return null
const apiKeyEnv = typeof section.apiKeyEnv === 'string' && section.apiKeyEnv !== ''
? section.apiKeyEnv
+52
View File
@@ -170,6 +170,26 @@ export interface Baseline {
transportFile: string
transportPresent: boolean
}
/**
* The balance capsule's host seam (src/host/balance.ts). The facts fold
* reads the DeepSeek API-key provider's settings row through whichever face
* the line serves: the `get(ns)` registered-section read through V3, the
* Config-form `describe()` projection on V4+ — where the provider row is
* the entry whose served value declares the top-level `apiKeyEnv`
* credential ref, preferred under the settings ids the generations have
* served it as. Each face is probed against the tag's real sources.
*/
balance: {
/** The settings service source the face needles are asserted in. */
settingsFile: string
/** The `get(ns)` section read — served through V3, retired on V4+. */
settingsGetNeedle: string
settingsGetPresent: boolean
/** The Config-form projection needle, when this line serves one the fold reads. */
settingsDescribeNeedle: string | undefined
/** The provider sources carrying the plugin id and the section shape the fold matches. */
providerFiles: readonly { file: string; needles: readonly string[] }[]
}
}
export const BASELINES: readonly Baseline[] = [
@@ -245,6 +265,18 @@ export const BASELINES: readonly Baseline[] = [
transportFile: 'packages/client/ui-settings/src/client/settings-scope.ts',
transportPresent: true,
},
balance: {
settingsFile: 'packages/settings/settings/src/index.ts',
settingsGetNeedle: 'get<const Namespace extends string>(ns:',
settingsGetPresent: true,
settingsDescribeNeedle: undefined,
providerFiles: [
{
file: 'packages/llm/llm-deepseek/src/index.ts',
needles: ["export const name = 'llm-deepseek'", "apiKeyEnv: z.string().role('credential-ref')"],
},
],
},
stepGuard: {
loopFile: 'packages/core/agent-loop/src/agent.ts',
loopNeedles: ["'agent/pre-step'", "append('user/message'"],
@@ -331,6 +363,26 @@ export const BASELINES: readonly Baseline[] = [
transportFile: 'packages/client/ui-settings/src/client/config-form.ts',
transportPresent: true,
},
balance: {
settingsFile: 'packages/settings/settings/src/index.ts',
settingsGetNeedle: 'get<const Namespace extends string>(ns:',
settingsGetPresent: false,
settingsDescribeNeedle: 'describe(options?: SettingsDescribeOptions)',
providerFiles: [
{
file: 'packages/llm/llm-deepseek-api-key/src/index.ts',
needles: ["export const name = 'llm-deepseek-api-key'"],
},
{
file: 'packages/llm/llm-deepseek-api-key/src/config.ts',
needles: ["apiKeyEnv: z.string().role('credential-ref').default('DEEPSEEK_API_KEY').volatile()"],
},
{
file: 'packages/llm/llm-deepseek/src/config.ts',
needles: ['baseURL: z.string().volatile()'],
},
],
},
stepGuard: {
loopFile: 'packages/core/agent-loop/src/agent.ts',
loopNeedles: ["'agent/pre-step'", "append('user/message'"],
+27
View File
@@ -115,6 +115,33 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base
assert.equal(pattern.test('dsh-context'), true)
})
test('balance: the settings face and the DeepSeek provider row the facts fold reads', () => {
// The balance route resolves the provider's settings row through the
// face this generation serves — the `get(ns)` section read through V3,
// the Config-form `describe()` projection on V4+ (the row matched by
// its own apiKeyEnv-bearing section shape). Presence asserted both
// ways, so a moved seam names itself instead of silently hiding the
// capsule.
const balance = baseline.balance
assert.equal(
staging.dshHasString(baseline.tag, balance.settingsGetNeedle, balance.settingsFile),
balance.settingsGetPresent,
'the settings service get face',
)
if (balance.settingsDescribeNeedle !== undefined) {
assert.equal(
staging.dshHasString(baseline.tag, balance.settingsDescribeNeedle, balance.settingsFile),
true,
'the settings service describe face',
)
}
for (const provider of balance.providerFiles) {
for (const needle of provider.needles) {
assert.equal(staging.dshHasString(baseline.tag, needle, provider.file), true, `provider seam: ${needle}`)
}
}
})
for (const slot of staging.SLOT_SEAMS) {
test(`client: slot "${slot}" exists`, () => {
assert.equal(staging.dshHasString(baseline.tag, slot, 'packages/client/*/src/**'), true)
+139
View File
@@ -69,6 +69,16 @@ function settingsOf(section: unknown): { get(ns: string): unknown } {
return { get: (ns: string) => (ns === 'llm-deepseek' ? section : undefined) }
}
/** The V4+ settings face projecting one row per configurable entry (`throws` fails the read). */
function settingsDescribeOf(rows: unknown, throws = false): { describe(): unknown } {
return {
describe() {
if (throws) throw new Error('hostile describe')
return rows
},
}
}
/** The harness credentials face serving one resolved value. */
function credentialsOf(value: unknown, calls?: string[]): { resolve(ref: string): Promise<{ value: unknown } | undefined> } {
return {
@@ -282,6 +292,135 @@ describe('balance route outcomes', () => {
})
})
describe('settings faces across generations', () => {
test('the V4+ describe face serves the facts by the provider row\'s own shape', async () => {
const refs: string[] = []
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: settingsDescribeOf([
{ ns: 'ui-theme', value: { preference: 'light' } },
{ ns: 'llm-pi-ai', value: { providers: { tokener: { apiKeyEnv: 'TOKENER_API_KEY' } } } },
{ ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://proxy.test///', thinking: 'enabled' } },
]),
credentials: credentialsOf('sk-v4', refs),
})
watchBalanceChannel(ctx)
const { log } = stubPlatform(CNY_BALANCE)
await serve(captured)
assert.deepEqual(refs, ['DEEPSEEK_API_KEY'])
assert.equal(log[0]?.url, 'https://proxy.test/user/balance')
assert.equal(log[0]?.auth, 'Bearer sk-v4')
})
test('a provider row under an unknown entry id serves by shape; the first shape wins', async () => {
const refs: string[] = []
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: settingsDescribeOf([
{ ns: 'renamed-a', value: { apiKeyEnv: 'FIRST_KEY' } },
{ ns: 'renamed-b', value: { apiKeyEnv: 'SECOND_KEY' } },
]),
credentials: credentialsOf('sk-shape', refs),
})
watchBalanceChannel(ctx)
const { log } = stubPlatform(CNY_BALANCE)
await serve(captured)
assert.deepEqual(refs, ['FIRST_KEY'])
assert.equal(log[0]?.url, 'https://api.deepseek.com/user/balance')
})
test('the served entry ids win over an earlier shape-only row', async () => {
const refs: string[] = []
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: settingsDescribeOf([
{ ns: 'some-provider', value: { apiKeyEnv: 'OTHER_KEY' } },
{ ns: 'llm-deepseek', value: { apiKeyEnv: 'PRODUCT_KEY' } },
]),
credentials: credentialsOf('sk-product', refs),
})
watchBalanceChannel(ctx)
stubPlatform(CNY_BALANCE)
await serve(captured)
assert.deepEqual(refs, ['PRODUCT_KEY'])
})
test('a V3 section that reads absent falls through to the describe face', async () => {
const refs: string[] = []
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: {
get: () => undefined,
describe: () => [{ ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'FALLBACK_KEY' } }],
},
credentials: credentialsOf('sk-fallback', refs),
})
watchBalanceChannel(ctx)
stubPlatform(CNY_BALANCE)
await serve(captured)
assert.deepEqual(refs, ['FALLBACK_KEY'])
})
test('the V3 section wins when both faces are served', async () => {
const refs: string[] = []
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: {
get: (ns: string) => (ns === 'llm-deepseek' ? { apiKeyEnv: 'V3_KEY' } : undefined),
describe: () => [{ ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'V4_KEY' } }],
},
credentials: credentialsOf('sk-both', refs),
})
watchBalanceChannel(ctx)
stubPlatform(CNY_BALANCE)
await serve(captured)
assert.deepEqual(refs, ['V3_KEY'])
})
test('every broken describe shape serves a typed null without touching the platform', async () => {
const faces: unknown[] = [
settingsDescribeOf('not an array'),
settingsDescribeOf(null),
settingsDescribeOf([]),
settingsDescribeOf([null, 'row', 42, [], {}]),
settingsDescribeOf([{ value: null }, { ns: 'llm-deepseek-api-key' }, { value: 'section' }]),
settingsDescribeOf([{ value: {} }, { value: { apiKeyEnv: '' } }, { value: { apiKeyEnv: 42 } }]),
settingsDescribeOf([], true),
]
for (const settings of faces) {
resetBalance()
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings,
credentials: credentialsOf('sk-test'),
})
watchBalanceChannel(ctx)
const { calls } = stubPlatform(CNY_BALANCE)
const reply = await serve(captured)
assert.deepEqual(reply, { ok: true, value: null })
assert.equal(calls(), 0)
}
})
test('a hostile row throwing on property access drops whole, valid siblings survive', async () => {
const refs: string[] = []
const hostile = new Proxy({}, { get() { throw new Error('hostile') } })
const { ctx, captured } = ctxOf({
connection: { fetch: { register: () => () => {} } },
settings: settingsDescribeOf([hostile, { ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'DEEPSEEK_API_KEY' } }]),
credentials: credentialsOf('sk-test', refs),
})
watchBalanceChannel(ctx)
stubPlatform(CNY_BALANCE)
const reply = await serve(captured)
assert.deepEqual(refs, ['DEEPSEEK_API_KEY'])
assert.deepEqual(reply, {
ok: true,
value: { isAvailable: true, balances: [{ currency: 'CNY', total: 110, granted: 10, toppedUp: 100 }] },
})
})
})
describe('balanceOfPayload (direct)', () => {
test('multiple currencies keep their entries in order', () => {
const balance = balanceOfPayload({