Files
dsh_shenxian/test/db.test.mjs
T
admin c2b7c5ef71 平台共享模型改为「管理员逐用户授权」+ 品牌中文名改「能力网络」+ 修掉跨机模型落地/语言偏好静默失效
三条线合并入库(同一次部署批次,源码与生产此前已一致):

一、档案 138 · 平台共享模型:管理员逐用户授权(默认关闭)
  用户口径原文:「admin 设置的共享模型,需要 admin 在用户列表中开启(新增选项,默认关闭),
  用户才能在会话中使用(以及在设置的模型设置页面展示)」。
  · DB 迁移 v11:新增 users.shared_model_granted(DEFAULT 0 = 默认关闭)。
    ⚠️ 刻意**不**复用 v6 的 shared_model_enabled —— 那是用户侧偏好(用户能自己关,默认 1),
    而本需求要的是**管理员门禁**;共用一列则用户点一下就给自己授权,门禁形同不存在。
    生效 = granted ∧ enabled(server.ts#sharedLandingRows)。
  · 新路由 POST /api/admin/users/:id/models/shared(requireAdmin)+ 审计 shared_model_grant
    + **尽力而为**重启该用户实例(租约被占/实例未运行都不算失败)。
  · admin 用户列表新增「共享模型」列;用户侧 /api/me/keys 增 shared.granted / sharedModelGranted;
    插件 0.3.24:未授权时「平台共享模型」整块不渲染。

二、顺带修掉一个既有真缺陷:平台侧写用户 home 必须走 UserFs(用户卷在 worker 上)
  landModels 原先 join(owner.home_dir, …) + 本机 fs ⇒ 对「实例不在控制面本机」的用户
  读到空串(**不报错**)⇒ 模型落地一直是**静默空操作**(托管清单还被清空)
  ——即档案 87 的模型设置页对 guest 这类用户**从未生效**。
  · UserFs 新增 readHomeFile/writeHomeFile + 文件名白名单(settings.yaml / .credentials.yaml)
  · worker agent 新增 /fs/home-read /fs/home-write(只认白名单裸文件名)
  · landModels 改走 userFs(与"文件面"同一份按归属路由 ⇒ 落地与实例必然同机)
  · 同轮把 /api/me/locale(档案 102 语言偏好)也改成同一套(原先同样失效)
  · home-files.ts 抽出 backupHomeFile(备份留平台侧,命名规则逐字不变)

三、档案 139 · 品牌中文名:能力枢纽 → 能力网络(其他语言仍 CapabilityNet)
  落点四处:i18n 中文词条 / admin.html 顶栏 / favicon.svg 的 title+aria-label / design.css 注释;
  test/i18n-brand.test.mjs 期望值同步。档案 137 顶部加"后续"指针,不改历史。

验证(全部真机实测):
  · 红腿:未授权 → 106 上 guest 的 .credentials.yaml refs 变空(共享 key 被撤)
  · 绿腿:授权 → key 回来 + 托管清单恢复 ["DEEPSEEK_API_KEY"]
  · admin 列表带出 sharedModelGranted;用户侧 granted 随授权翻面(true/shared ↔ false/none)
  · 开关两次均 200(不再假失败);插件实装 0.3.24 且含 gating 字符串
  · 语言偏好:106 上 settings.yaml 出现 locale.preference=en(属主=实例属主,既有段逐字保留)
  · 本机 npm test 226 tests / 225 pass / 0 fail / 1 skipped;四个 verify 脚本全绿

部署:47 推 51 个 lib 产物、106 推 12 个(lib/ 是 gitignore ⇒ 回滚点物化在
/opt/dsh/backups/seq138b-20260919-125345/,逐文件对账 0 不一致;先 106 后 47);
插件 business-plugins 0.3.24(两机 artifacts 与本机 pack md5 一致)。
2026-09-19 13:51:01 +08:00

315 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DB adapter regression tests (node:test). Runs against the built `lib/`
// output — `npm test` builds first. Covers the constraint-mapping and
// transaction semantics shared by both backends:
// - unique / foreign-key errors converge on UniqueViolationError /
// ForeignKeyViolationError
// - the "disable-all-then-enable-one" credential upsert keeps exactly one
// enabled key under concurrent writes
// - concurrent createUser / audit writes land cleanly
// The Postgres suite self-skips unless DSHS_TEST_DB_URL is set
// (mirrors the CI e2e self-skip convention).
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { SqliteAdapter } from '../lib/db/sqlite.js'
import { openPgAdapter } from '../lib/db/pg.js'
import { ForeignKeyViolationError, UniqueViolationError } from '../lib/db/errors.js'
const user = (id, username) => ({ id, username, passHash: 'x', role: 'active', homeDir: `/home/${username}` })
function register(backend, makeAdapter) {
test(`${backend}: duplicate username → UniqueViolationError`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await assert.rejects(() => db.createUser(user('b', 'alice')), UniqueViolationError)
} finally {
await db.close()
}
})
test(`${backend}: session for unknown user → ForeignKeyViolationError`, async () => {
const db = await makeAdapter()
try {
await assert.rejects(
() => db.createSession({ tokenHash: 't', userId: 'missing', expiresAt: Date.now() + 1000 }),
ForeignKeyViolationError,
)
} finally {
await db.close()
}
})
// 档案 87:条目口径从「互斥单选」改为「**各自开关、可同时启用**」⇒ 老断言整体改写。
test(`${backend}: concurrent setCredentialKey keeps every entry enabled (不再互斥)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await Promise.all(
Array.from({ length: 5 }, (_, i) => db.setCredentialKey('a', `k${i}`, `ref${i}`)),
)
const keys = await db.listCredentialKeys('a')
assert.equal(keys.length, 5, 'five rows')
// 写新条目**不再**把其它条目全关(老实现是 `SET enabled = 0 WHERE user_id = ?`)。
assert.equal(keys.filter((k) => k.enabled).length, 5, 'every entry stays enabled')
assert.equal((await db.listEnabledCredentialKeys('a')).length, 5, 'listEnabled agrees with list')
const ref = await db.getEnabledCredentialKeyRef('a')
assert.ok(ref !== null && ref.startsWith('ref'), 'enabled key has a ref')
} finally {
await db.close()
}
})
test(`${backend}: toggleCredentialKey 只动一行(不影响其它条目)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
const k1 = await db.setCredentialKey('a', 'k1', 'r1')
await db.setCredentialKey('a', 'k2', 'r2')
assert.equal(await db.toggleCredentialKey('a', k1.id, false), true)
const keys = await db.listCredentialKeys('a')
assert.equal(keys.find((k) => k.id === k1.id).enabled, false, 'target row off')
assert.equal(keys.find((k) => k.name === 'k2').enabled, true, 'the other row untouched')
assert.equal((await db.listEnabledCredentialKeys('a')).length, 1, 'only one enabled now')
// 不存在的 id ⇒ false(路由据此回 404)
assert.equal(await db.toggleCredentialKey('a', 'nope', true), false)
} finally {
await db.close()
}
})
test(`${backend}: getEnabledCredentialKeyRef 只认内置条目(档案 87 语义重定义)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
// 自定义厂家(给了 baseUrl)**不是**"用户自己的 DeepSeek key",否则 keySourceOf /
// resolveApiKey 会把一个网关的 key 当成平台内置 key 用。
await db.setCredentialKey('a', 'gw', 'gwref', {
route: 'my-gw',
baseUrl: 'https://api.example.com/v1',
api: 'openai-completions',
models: '["gpt-4o"]',
})
assert.equal(await db.getEnabledCredentialKeyRef('a'), null, 'custom provider is not the builtin ref')
await db.setCredentialKey('a', 'ds', 'dsref')
assert.equal(await db.getEnabledCredentialKeyRef('a'), 'dsref', 'builtin entry wins')
// 元数据必须原样存回来(route / baseUrl / api / models)
const gw = (await db.listCredentialKeys('a')).find((k) => k.name === 'gw')
assert.equal(gw.route, 'my-gw')
assert.equal(gw.baseUrl, 'https://api.example.com/v1')
assert.equal(gw.api, 'openai-completions')
assert.equal(gw.models, '["gpt-4o"]')
} finally {
await db.close()
}
})
test(`${backend}: sharedModelEnabled 默认开、可关(档案 87 口径②)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
assert.equal(await db.getSharedModelEnabled('a'), true, 'V6 默认 true')
assert.equal(await db.setSharedModelEnabled('a', false), true)
assert.equal(await db.getSharedModelEnabled('a'), false)
assert.equal(await db.setSharedModelEnabled('a', true), true)
assert.equal(await db.getSharedModelEnabled('a'), true)
} finally {
await db.close()
}
})
// 档案 138(v11):管理员逐用户授权 —— **默认关闭**、可开可关、且与用户偏好**互相独立**。
// 这三条判据是门禁的全部内容;它们任一被写反(尤其"默认"那条)都等于门禁不存在。
test(`${backend}: sharedModelGranted 默认关、可开可关,且与用户偏好互不影响(档案 138)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
assert.equal(await db.getSharedModelGranted('a'), false, 'v11 默认 false(授权默认关闭)')
assert.equal(await db.setSharedModelGranted('a', true), true)
assert.equal(await db.getSharedModelGranted('a'), true)
// 用户偏好仍是它自己的默认值 —— 授权不改变偏好(两列互不覆盖)。
assert.equal(await db.getSharedModelEnabled('a'), true, '授权不改动用户侧偏好')
assert.equal(await db.setSharedModelGranted('a', false), true)
assert.equal(await db.getSharedModelGranted('a'), false)
// 未知用户:授权按 false(**失败关闭**),偏好按 true(宁可多给)—— 两条故意相反,钉死它。
assert.equal(await db.getSharedModelGranted('nobody'), false)
assert.equal(await db.getSharedModelEnabled('nobody'), true)
} finally {
await db.close()
}
})
test(`${backend}: listPublicUsers 带出 sharedModelGranted(admin 列表要用)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await db.setSharedModelGranted('a', true)
const users = await db.listPublicUsers()
assert.equal(users.length, 1)
assert.equal(users[0].sharedModelGranted, true)
} finally {
await db.close()
}
})
test(`${backend}: selectCredentialKey 不再关掉别的条目(档案 87)`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
const k1 = await db.setCredentialKey('a', 'k1', 'r1')
const k2 = await db.setCredentialKey('a', 'k2', 'r2')
// 两条内置条目都启用 ⇒ 取"最新一条"(两条写在同一毫秒时由 id 决定,故这里只断非空)
assert.ok((await db.getEnabledCredentialKeyRef('a')) !== null)
assert.equal(await db.selectCredentialKey('a', k1.id), true)
// 老语义会先把所有条目关掉再开这一个;新语义只保证"这一个开"。
const keys = await db.listCredentialKeys('a')
assert.equal(keys.find((k) => k.id === k2.id).enabled, true, 'another entry is not switched off')
assert.equal(keys.find((k) => k.id === k1.id).enabled, true, 'target stays enabled')
} finally {
await db.close()
}
})
test(`${backend}: concurrent createUser`, async () => {
const db = await makeAdapter()
try {
await Promise.all(Array.from({ length: 20 }, (_, i) => db.createUser(user(`u${i}`, `user${i}`))))
assert.equal((await db.listPublicUsers()).length, 20)
} finally {
await db.close()
}
})
test(`${backend}: getOrCreateWorkspace is idempotent`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
const w1 = await db.getOrCreateWorkspace('a', 'proj/one')
const w2 = await db.getOrCreateWorkspace('a', 'proj/one')
assert.equal(w1.id, w2.id)
} finally {
await db.close()
}
})
test(`${backend}: createUser assigns a unique uid`, async () => {
const db = await makeAdapter()
try {
const a = await db.createUser(user('a', 'alice'))
const b = await db.createUser(user('b', 'bob'))
assert.ok(a.uid !== null && a.uid !== undefined, 'first user has a uid')
assert.notEqual(a.uid, b.uid, 'uids are unique across users')
assert.equal((await db.listUsersWithoutUid()).length, 0, 'no unassigned uids remain')
} finally {
await db.close()
}
})
test(`${backend}: concurrent audit writes`, async () => {
const db = await makeAdapter()
try {
await Promise.all(Array.from({ length: 10 }, (_, i) => db.audit('system', 'test', `detail-${i}`)))
} finally {
await db.close()
}
})
test(`${backend}: upsertInstance round-trips folder + patch and is idempotent`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await db.upsertInstance({ id: 'dsh-a', userId: 'a', role: 'main', status: 'starting', folder: '/ws/proj', patch: '- insert:\n' })
const first = await db.findInstance('dsh-a')
assert.equal(first.folder, '/ws/proj')
assert.equal(first.patch, '- insert:\n')
assert.equal(first.status, 'starting')
assert.ok(first.startedAt > 0, 'started_at stamped')
// Same deterministic id → overwrite, not a duplicate row.
await db.upsertInstance({ id: 'dsh-a', userId: 'a', role: 'main', status: 'running', folder: '/ws/other' })
const second = await db.findInstance('dsh-a')
assert.equal(second.folder, '/ws/other')
assert.equal(second.patch, null, 'omitted patch clears the column')
assert.equal((await db.listInstancesByRole('main')).filter((i) => i.userId === 'a').length, 1)
} finally {
await db.close()
}
})
test(`${backend}: setInstanceStatus records the exit outcome`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await db.upsertInstance({ id: 'dsh-a', userId: 'a', role: 'main', status: 'running', folder: '/ws' })
assert.equal(await db.setInstanceStatus('dsh-a', 'crashed', { exitCode: 137, lastError: 'OOMKilled' }), true)
const row = await db.findInstance('dsh-a')
assert.equal(row.status, 'crashed')
assert.equal(row.exitCode, 137)
assert.equal(row.lastError, 'OOMKilled')
assert.ok(row.lastExit > 0, 'last_exit stamped')
assert.equal(await db.setInstanceStatus('dsh-missing', 'stopped'), false, 'unknown id reports no change')
} finally {
await db.close()
}
})
test(`${backend}: instances are scoped by user and role, and cascade on user delete`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
await db.createUser(user('b', 'bob'))
await db.upsertInstance({ id: 'dsh-a', userId: 'a', role: 'main', status: 'running', folder: '/ws' })
await db.upsertInstance({ id: 'dsh-a-watchdog', userId: 'a', role: 'watchdog', status: 'starting' })
await db.upsertInstance({ id: 'dsh-b', userId: 'b', role: 'main', status: 'running', folder: '/ws' })
assert.equal((await db.findUserInstance('a', 'main')).id, 'dsh-a')
assert.equal((await db.findUserInstance('a', 'watchdog')).id, 'dsh-a-watchdog')
assert.equal((await db.listInstancesByRole('main')).length, 2)
assert.equal((await db.listInstancesByRole('watchdog')).length, 1)
await db.deleteUserInstances('a')
assert.equal(await db.findUserInstance('a', 'main'), undefined)
assert.equal((await db.listInstancesByRole('main')).length, 1, "b's instance survives")
} finally {
await db.close()
}
})
test(`${backend}: hasActiveSession reflects unexpired vs expired sessions`, async () => {
const db = await makeAdapter()
try {
await db.createUser(user('a', 'alice'))
assert.equal(await db.hasActiveSession('a'), false, 'no session → inactive')
await db.createSession({ tokenHash: 't1', userId: 'a', expiresAt: Date.now() + 60_000 })
assert.equal(await db.hasActiveSession('a'), true, 'unexpired session → active')
await db.createSession({ tokenHash: 't2', userId: 'a', expiresAt: Date.now() - 1000 })
assert.equal(await db.hasActiveSession('a'), true, 'at least one unexpired session → active')
await db.deleteSession('t1')
assert.equal(await db.hasActiveSession('a'), false, 'only expired session left → inactive')
} finally {
await db.close()
}
})
test(`${backend}: instance for unknown user → ForeignKeyViolationError`, async () => {
const db = await makeAdapter()
try {
await assert.rejects(
() => db.upsertInstance({ id: 'dsh-ghost', userId: 'missing', role: 'main', status: 'starting' }),
ForeignKeyViolationError,
)
} finally {
await db.close()
}
})
}
register('sqlite', () => new SqliteAdapter(':memory:', 100000))
const pgUrl = process.env.DSHS_TEST_DB_URL
if (pgUrl) {
register('pg', () => openPgAdapter(pgUrl, 100000))
} else {
test('pg: skipped — set DSHS_TEST_DB_URL to run', { skip: 'no DSHS_TEST_DB_URL' }, () => {})
}