chore: 文档库全量重新入库(2026-10-20)
build / build-and-scan (push) Canceled after 0s

This commit is contained in:
admin committed 2026-10-20 15:05:55 +08:00
commit 5ec99ce227
668 files changed
+164448

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
// 插件/客户端接入 SDK(`sdk/im-plugin-client`)的契约测试。
//
// 盯三件事(都是"插件侧会踩、且踩了很难查"的):
// ① 凭据头与插件身份头的**注入**(`x-dsh-im-instance-token` / `x-dsh-im-plugin-id`);
// ② 失败一律折成 `{ok:false,reason,detail}` —— 插件侧只需判 `ok`,⛔ 不必维护状态码表;
// ③ 未配置时**立刻**回 `not-registered`,⛔ 不发请求、⛔ 不抛异常。
//
// 跑法:`npm test`。
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createServer } from 'node:http'
import { SDK_VERSION, createImClient } from '../sdk/im-plugin-client/index.mjs'
/** 起一个假平台:记下请求,按剧本回。 */
async function withServer(handler, fn) {
const seen = []
const server = createServer((req, res) => {
let body = ''
req.on('data', (c) => (body += c))
req.on('end', () => {
const record = { method: req.method, url: req.url, headers: req.headers, body }
seen.push(record)
const out = handler(record)
res.writeHead(out.status ?? 200, { 'content-type': 'application/json' })
res.end(out.body === undefined ? '' : JSON.stringify(out.body))
})
})
await new Promise((r) => server.listen(0, '127.0.0.1', r))
const baseUrl = `http://127.0.0.1:${server.address().port}`
try {
return await fn(baseUrl, seen)
} finally {
await new Promise((r) => server.close(r))
}
}
test('im-plugin-client: 注入了实例凭据头与插件身份头', async () => {
await withServer(
() => ({ body: { ok: true, value: [{ note_id: 'n1' }] } }),
async (baseUrl, seen) => {
const im = createImClient({ baseUrl: baseUrl + '/', token: 'tok-1', pluginId: '@dsh-local/trpg-kit' })
const r = await im.data.table('my_notes').find({ eq: { body: 'x' }, limit: 5, afterCursor: 'n0', order: 'desc' })
assert.equal(r.ok, true)
assert.deepEqual(r.value, [{ note_id: 'n1' }])
const req = seen[0]
assert.equal(req.method, 'GET')
assert.equal(req.headers['x-dsh-im-instance-token'], 'tok-1')
assert.equal(req.headers['x-dsh-im-plugin-id'], '@dsh-local/trpg-kit')
assert.equal(req.url, '/api/im/data/my_notes?eq.body=x&after=n0&order=desc&limit=5')
},
)
})
test('im-plugin-client: 写与改与删走对方法与路径(表名不带 p_ 前缀)', async () => {
await withServer(
() => ({ body: { ok: true, value: {} } }),
async (baseUrl, seen) => {
const im = createImClient({ baseUrl, token: 't', pluginId: 'p' })
const t = im.data.table('character_sheet')
await t.insert({ pc_name: 'a', room_id: 'r1' })
await t.update('a', { sheet: { hp: 3 } })
await t.remove('a')
await t.count({ eq: { room_id: 'r1' } })
assert.deepEqual(seen.map((s) => `${s.method} ${s.url}`), [
'POST /api/im/data/character_sheet',
'PATCH /api/im/data/character_sheet/a',
'DELETE /api/im/data/character_sheet/a',
'GET /api/im/data/character_sheet/count?eq.room_id=r1',
])
assert.deepEqual(JSON.parse(seen[0].body), { pc_name: 'a', room_id: 'r1' })
},
)
})
test('im-plugin-client: 契约失败原样透传(200 + ok:false)', async () => {
await withServer(
() => ({ body: { ok: false, reason: 'quota-exceeded', detail: '超过单插件行数上限 200000' } }),
async (baseUrl) => {
const im = createImClient({ baseUrl, token: 't', pluginId: 'p' })
const r = await im.data.table('my_notes').insert({ note_id: 'n' })
assert.equal(r.ok, false)
assert.equal(r.reason, 'quota-exceeded')
assert.match(r.detail, /行数上限/)
},
)
})
test('im-plugin-client: 非 200(身份 / 就绪)也折成同一个 ok:false 形状', async () => {
await withServer(
(req) => (req.url.includes('status')
? { status: 409, body: { error: 'plugin-data-unavailable', reason: 'datastore-not-ready', detail: '尚未建库' } }
: { status: 401, body: { error: 'instance-token-rejected', reason: 'unknown' } }),
async (baseUrl) => {
const im = createImClient({ baseUrl, token: 'bad', pluginId: 'p' })
const s = await im.data.status()
assert.equal(s.ok, false)
assert.equal(s.reason, 'disabled')
assert.equal(s.httpStatus, 409)
assert.match(s.detail, /尚未建库/)
const r = await im.rooms.list()
assert.equal(r.reason, 'not-registered', '401 ⇒ not-registered(凭据不认)')
},
)
})
test('im-plugin-client: 未配置时立刻回 not-registered —— ⛔ 不发请求、⛔ 不抛', async () => {
await withServer(
() => {
throw new Error('不应该发出请求')
},
async (baseUrl, seen) => {
const im = createImClient({ baseUrl: '', token: 'super-secret-1', pluginId: 'p' })
assert.equal(im.configured, false)
const r = await im.data.table('my_notes').find()
assert.equal(r.ok, false)
assert.equal(r.reason, 'not-registered')
assert.equal(seen.length, 0)
assert.ok(!String(im).includes('super-secret-1'), '诊断串⛔ 不含 token 明文')
assert.match(String(im), /token=已注入/, '只回"有没有配"')
},
)
})
test('im-plugin-client: 版本号可读(插件报障要带它)', () => {
assert.match(SDK_VERSION, /^\d+\.\d+\.\d+$/)
})