Files
dsh_shenxian/test/im-plugin-client.test.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

130 lines
5.2 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.
// 插件/客户端接入 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+$/)
})