Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ export function apply(ctx: Context, config: Config = {}): void {
const pricingFile = config.pricingFile?.trim() || defaultPricingFile()
const fileSource = filePricingSource(pricingFile)
const resolver = createPricingResolver(fileSource)
ctx.effect(() => fileSource.dispose(), 'dsh-usage-chart: pricing file watcher')
// 注意返回的是「返回 disposer 的函数」:写成 () => fileSource.dispose() 会在注册时立刻销毁文件源。
ctx.effect(() => () => fileSource.dispose(), 'dsh-usage-chart: pricing file watcher')

ctx.effect(() => ctx.webServer.register({
kind: 'exact',
Expand Down
10 changes: 7 additions & 3 deletions tests/core.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import test, { after } from 'node:test'

import {
DEFAULT_CNY_PER_USD,
Expand All @@ -16,6 +16,10 @@ import {
normalizeCurrency,
} from '../lib/index.js'

/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */
const disposers = []
after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() })

function responseRecorder() {
return {
headers: {},
Expand All @@ -30,7 +34,7 @@ function responseRecorder() {
function mountedRoutes(events = [], config = {}) {
const routes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: {
register(route) { routes.set(route.path, route); return () => {} },
Expand Down Expand Up @@ -126,7 +130,7 @@ test('mounted Host routes enforce method and browser-origin guards', async () =>
async function balanceResponse(extraCtx = {}, config = {}) {
const routes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get(name) { return name === 'credentials' ? extraCtx.credentials : undefined },
webServer: { register(route) { routes.set(route.path, route); return () => {} } },
sessions: { get() { return undefined } },
Expand Down
46 changes: 44 additions & 2 deletions tests/pricing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import test, { after } from 'node:test'

import {
BUILTIN_VERIFIED_AT,
Expand All @@ -19,6 +19,10 @@ import {
tierAt,
} from '../lib/index.js'

/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */
const disposers = []
after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() })

/** 2026-09-10 内置刊例价(双币种 /1M,高峰/空闲)。 */
const FLASH_OFF_PEAK = {
cny: { cacheMissInput: 1, cacheHitInput: 0.02, output: 4 },
Expand Down Expand Up @@ -264,7 +268,7 @@ function responseRecorder() {
test('/pricing route exposes builtin + fallback + models snapshot', async () => {
const routes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: { register(route) { routes.set(route.path, route); return () => {} } },
sessions: { get() { return undefined } },
Expand All @@ -286,3 +290,41 @@ test('/pricing route exposes builtin + fallback + models snapshot', async () =>
assert.ok(models.includes('deepseek-v4-pro'))
assert.ok(models.includes('deepseek-v4-flash-vision-exp'))
})

test('/pricing route serves the user pricing.json override (file source survives startup)', async () => {
const dir = await mkdtemp(join(tmpdir(), 'duc-pricing-'))
const pricingFile = join(dir, 'pricing.json')
await writeFile(pricingFile, JSON.stringify({
models: {
'deepseek-flash': {
peak: { cny: { cacheMissInput: 19.99, cacheHitInput: 0.04, output: 8 }, usd: { cacheMissInput: 2.99, cacheHitInput: 0.006, output: 1.2 } },
offPeak: { cny: { cacheMissInput: 9.99, cacheHitInput: 0.02, output: 4 }, usd: { cacheMissInput: 1.49, cacheHitInput: 0.003, output: 0.6 } },
},
},
}))
const routes = new Map()
const disposers = []
try {
apply({
// cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时才调用。
// 写成 () => source.dispose() 会在这里当场销毁文件源 → 覆盖文件永不生效(本测试即为此回归)。
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: { register(route) { routes.set(route.path, route); return () => {} } },
sessions: { get() { return undefined } },
}, { pricingFile })
await new Promise((resolve) => setTimeout(resolve, 50))

const recorder = responseRecorder()
await routes.get('/dsh-usage-chart/pricing').handler(
{ method: 'GET', url: '/dsh-usage-chart/pricing', headers: { host: 'localhost:3000' } },
recorder,
)
const flash = JSON.parse(recorder.body).models.find((m) => m.model === 'deepseek-flash')
assert.equal(flash.source, 'file')
assert.equal(flash.pricing.offPeak.cny.cacheMissInput, 9.99)
} finally {
for (const dispose of disposers) if (typeof dispose === 'function') dispose()
await rm(dir, { recursive: true, force: true })
}
})
13 changes: 9 additions & 4 deletions tests/rounds.test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import test, { after } from 'node:test'

import { apply, foldRounds, foldTurnUsage, tierAt } from '../lib/index.js'

/** cordis 语义:effect(setup) 立即执行 setup,把返回的 disposer 留到 fiber 销毁时再调用。 */
const disposers = []
after(() => { for (const dispose of disposers) if (typeof dispose === 'function') dispose() })


/** 2026-09-10 内置刊例价(双币种 /1M):flash 高峰/空闲。 */
const FLASH_PEAK = {
cny: { cacheMissInput: 2, cacheHitInput: 0.04, output: 8 },
Expand Down Expand Up @@ -206,7 +211,7 @@ test('/usage route returns rounds with cost and timing', async () => {
]
const routes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: { register(route) { routes.set(route.path, route); return () => {} } },
sessions: { get(id) { return id === 'session-test' ? { id, events } : undefined } },
Expand Down Expand Up @@ -247,7 +252,7 @@ test('/usage route prefers snapshotEvents() when the new API is present', async
]
const routes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: { register(route) { routes.set(route.path, route); return () => {} } },
// 有 snapshotEvents、无 events —— 必须走 snapshotEvents() 分支,且绝不触碰 events 属性。
Expand All @@ -273,7 +278,7 @@ test('/usage route prefers snapshotEvents() when the new API is present', async
const both = { id: 'session-test', events: [], snapshotEvents: () => events }
const bothRoutes = new Map()
apply({
effect(setup) { setup() },
effect(setup) { disposers.push(setup()) },
get() { return undefined },
webServer: { register(route) { bothRoutes.set(route.path, route); return () => {} } },
sessions: { get(id) { return id === 'session-test' ? both : undefined } },
Expand Down