build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
415 lines
18 KiB
JavaScript
415 lines
18 KiB
JavaScript
// 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()
|
||
}
|
||
})
|
||
|
||
// ── v12 · 设备台账(序㊻ 步骤 6 / S3)────────────────────────────────────────
|
||
test(`${backend}: upsertOverlayDevice —— 首签 = 1、续签 = +1(同一行)、租约被刷新`, async () => {
|
||
const db = await makeAdapter()
|
||
try {
|
||
await db.createUser(user('u1', 'u1'))
|
||
const t0 = 1_700_000_000_000
|
||
const base = { network: 'u:u1', hostId: 'd-u1-abcdef12', userId: 'u1', nodeKey: 'ab'.repeat(32) }
|
||
|
||
const first = await db.upsertOverlayDevice({ ...base, leaseExpiresAt: t0 + 1000, at: t0 })
|
||
assert.equal(first.grantRenewals, 1, '首次插入 = 1')
|
||
assert.equal(first.status, 'active', '首次插入 = active')
|
||
assert.equal(first.createdAt, t0)
|
||
assert.equal(first.grantIssuedAt, t0)
|
||
|
||
const second = await db.upsertOverlayDevice({ ...base, leaseExpiresAt: t0 + 2000, at: t0 + 500 })
|
||
assert.equal(second.grantRenewals, 2, '续签 = 旧值 + 1(⛔ 不是恒为 1,也不是 2 卡住)')
|
||
assert.equal(second.leaseExpiresAt, t0 + 2000, '租约必须被刷新')
|
||
assert.equal(second.createdAt, t0, 'created_at 不被续签改动')
|
||
const third = await db.upsertOverlayDevice({ ...base, leaseExpiresAt: t0 + 3000, at: t0 + 900 })
|
||
assert.equal(third.grantRenewals, 3, '第三次 = 3(计数真的在累加)')
|
||
assert.equal((await db.listOverlayDevices('u:u1')).length, 1, '续签 = 同一行,⛔ 不是插新行')
|
||
|
||
// 按网过滤 / 按 userId 归属
|
||
await db.createUser(user('u2', 'u2'))
|
||
await db.upsertOverlayDevice({
|
||
network: 'u:u2',
|
||
hostId: 'd-u2-11111111',
|
||
userId: 'u2',
|
||
nodeKey: 'cd'.repeat(32),
|
||
leaseExpiresAt: t0 + 1000,
|
||
at: t0,
|
||
})
|
||
assert.equal((await db.listOverlayDevices()).length, 2, '不带过滤 = 全部')
|
||
assert.equal((await db.listOverlayDevices('u:u2')).length, 1, '按网过滤')
|
||
assert.equal((await db.findOverlayDevice('u:u2', 'd-u2-11111111')).userId, 'u2')
|
||
assert.equal(await db.findOverlayDevice('u:u2', 'd-u2-22222222'), undefined, '不存在 ⇒ undefined(⛔ 不抛)')
|
||
} finally {
|
||
await db.close()
|
||
}
|
||
})
|
||
|
||
test(`${backend}: setOverlayDeviceStatus —— 停发是**黏性**的(续签不复活)+ 不存在的行 = false`, async () => {
|
||
const db = await makeAdapter()
|
||
try {
|
||
await db.createUser(user('u3', 'u3'))
|
||
const row = {
|
||
network: 'u:u3',
|
||
hostId: 'd-u3-abcdef12',
|
||
userId: 'u3',
|
||
nodeKey: 'ef'.repeat(32),
|
||
leaseExpiresAt: Date.now() + 1000,
|
||
}
|
||
await db.upsertOverlayDevice(row)
|
||
assert.equal(await db.setOverlayDeviceStatus('u:u3', 'd-u3-abcdef12', 'revoked'), true)
|
||
assert.equal((await db.findOverlayDevice('u:u3', 'd-u3-abcdef12')).status, 'revoked')
|
||
// 🔴 关键:续签**不许**把它复活(SQL 层的 DO UPDATE SET 里没有 status 这一列)
|
||
const renewed = await db.upsertOverlayDevice({ ...row, leaseExpiresAt: Date.now() + 9999 })
|
||
assert.equal(renewed.status, 'revoked', '⚠️ 续签不得让被停发的设备自动复活')
|
||
assert.equal(renewed.grantRenewals, 2, '但它确实是一次续签(计数照常累加)')
|
||
// 恢复只能显式来
|
||
assert.equal(await db.setOverlayDeviceStatus('u:u3', 'd-u3-abcdef12', 'active'), true)
|
||
assert.equal((await db.findOverlayDevice('u:u3', 'd-u3-abcdef12')).status, 'active')
|
||
// 不存在的行 ⇒ false(路由据此回 404)
|
||
assert.equal(await db.setOverlayDeviceStatus('u:u3', 'd-u3-00000000', 'revoked'), false)
|
||
} finally {
|
||
await db.close()
|
||
}
|
||
})
|
||
|
||
test(`${backend}: overlay_devices 归属 user(删除用户即级联清理台账)`, async () => {
|
||
const db = await makeAdapter()
|
||
try {
|
||
await db.createUser(user('u4', 'u4'))
|
||
await db.upsertOverlayDevice({
|
||
network: 'u:u4',
|
||
hostId: 'd-u4-abcdef12',
|
||
userId: 'u4',
|
||
nodeKey: 'aa'.repeat(32),
|
||
leaseExpiresAt: Date.now() + 1000,
|
||
})
|
||
assert.equal((await db.listOverlayDevices()).length, 1)
|
||
await db.deleteUser('u4')
|
||
assert.equal((await db.listOverlayDevices()).length, 0, '删用户应当把台账一起带走(ON DELETE CASCADE)')
|
||
// 未知用户 ⇒ 外键违例(与其它表同一口径)
|
||
await assert.rejects(
|
||
() =>
|
||
db.upsertOverlayDevice({
|
||
network: 'u:ghost',
|
||
hostId: 'd-ghost-abcdef12',
|
||
userId: 'missing',
|
||
nodeKey: 'bb'.repeat(32),
|
||
leaseExpiresAt: Date.now(),
|
||
}),
|
||
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' }, () => {})
|
||
}
|