mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-16 10:30:45 +08:00
feat: add native result-file delivery
This commit is contained in:
parent
2803bbcbab
commit
d472dd5a75
65 changed files with 10205 additions and 255 deletions
|
|
@ -116,6 +116,162 @@ test('Discord API retries one rate-limited message request', async () => {
|
|||
assert.equal(attempts, 2);
|
||||
});
|
||||
|
||||
test('Discord API uploads a result file as a native attachment and preserves the reply', async () => {
|
||||
let request;
|
||||
const api = new DiscordApi({
|
||||
token: TOKEN,
|
||||
fetchImpl: async (url, options) => {
|
||||
request = { url, options };
|
||||
return jsonResponse({ id: '987654321012345679', attachments: [{ id: '1' }] });
|
||||
},
|
||||
});
|
||||
const result = await api.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
replyToMessageId: '123456789012345679',
|
||||
file: {
|
||||
artifactId: 'artifact-discord-one',
|
||||
deliveryKey: 'session:turn:artifact-discord-one',
|
||||
fileName: 'result.html',
|
||||
mediaType: 'text/html',
|
||||
bytes: Buffer.from('<p>discord-result</p>'),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.id, '987654321012345679');
|
||||
assert.match(request.url.pathname, /channels\/123456789012345678\/messages$/);
|
||||
assert.equal(request.options.headers['content-type'], undefined);
|
||||
assert.ok(request.options.body instanceof FormData);
|
||||
const payload = JSON.parse(request.options.body.get('payload_json'));
|
||||
assert.deepEqual(payload.attachments, [{ id: 0, filename: 'result.html' }]);
|
||||
assert.match(payload.nonce, /^[0-9a-f]{25}$/);
|
||||
assert.equal(payload.enforce_nonce, true);
|
||||
assert.equal(payload.message_reference.message_id, '123456789012345679');
|
||||
assert.deepEqual(payload.allowed_mentions, { parse: [], replied_user: false });
|
||||
const attachment = request.options.body.get('files[0]');
|
||||
assert.equal(attachment.name, 'result.html');
|
||||
assert.equal(attachment.type, 'text/html');
|
||||
assert.equal(Buffer.from(await attachment.arrayBuffer()).toString(), '<p>discord-result</p>');
|
||||
});
|
||||
|
||||
test('Discord attachment retry reuses one FormData body and one stable nonce', async () => {
|
||||
const bodies = [];
|
||||
const nonces = [];
|
||||
let attempts = 0;
|
||||
const api = new DiscordApi({
|
||||
token: TOKEN,
|
||||
fetchImpl: async (_url, options) => {
|
||||
attempts += 1;
|
||||
bodies.push(options.body);
|
||||
nonces.push(JSON.parse(options.body.get('payload_json')).nonce);
|
||||
return attempts === 1
|
||||
? jsonResponse({ code: 20028, message: 'rate limited', retry_after: 0.001 }, 429)
|
||||
: jsonResponse({ id: '987654321012345680', attachments: [{ id: '1' }] });
|
||||
},
|
||||
});
|
||||
const result = await api.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
file: {
|
||||
deliveryKey: 'session:turn:artifact-retry',
|
||||
fileName: 'result.txt',
|
||||
bytes: Buffer.from('retry-safe'),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.id, '987654321012345680');
|
||||
assert.equal(attempts, 2);
|
||||
assert.equal(bodies[0], bodies[1]);
|
||||
assert.equal(nonces[0], nonces[1]);
|
||||
assert.match(nonces[0], /^[0-9a-f]{25}$/);
|
||||
});
|
||||
|
||||
test('Discord attachment errors retain provider details and use stable artifact reasons', async () => {
|
||||
const cases = [{
|
||||
body: { code: 50013, message: 'Missing Permissions' },
|
||||
status: 403,
|
||||
code: 'artifact-permission-required',
|
||||
}, {
|
||||
body: { code: 40005, message: 'Request entity too large' },
|
||||
status: 413,
|
||||
code: 'artifact-too-large',
|
||||
}, {
|
||||
body: { code: 20028, message: 'rate limited', retry_after: 0.001 },
|
||||
status: 429,
|
||||
code: 'artifact-rate-limited',
|
||||
retryAfter: 0.001,
|
||||
}, {
|
||||
body: { code: 50035, message: 'Invalid Form Body' },
|
||||
status: 400,
|
||||
code: 'artifact-provider-rejected',
|
||||
}, {
|
||||
body: { code: 0, message: 'Internal Server Error' },
|
||||
status: 500,
|
||||
code: 'artifact-delivery-uncertain',
|
||||
}];
|
||||
|
||||
for (const entry of cases) {
|
||||
const api = new DiscordApi({
|
||||
token: TOKEN,
|
||||
fetchImpl: async () => jsonResponse(entry.body, entry.status),
|
||||
});
|
||||
await assert.rejects(() => api.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
file: { fileName: 'result.bin', bytes: Buffer.from('result') },
|
||||
}), (error) => {
|
||||
assert.equal(error.code, entry.code);
|
||||
assert.equal(error.providerCode, entry.body.code);
|
||||
assert.equal(error.status, entry.status);
|
||||
assert.equal(error.retry_after, entry.retryAfter);
|
||||
assert.equal(error.retryAfter, entry.retryAfter);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Discord attachment delivery marks post-dispatch failures uncertain but preserves caller aborts', async () => {
|
||||
for (const fetchImpl of [
|
||||
async () => { throw new TypeError('socket reset'); },
|
||||
async () => new Response('not-json', { status: 200 }),
|
||||
]) {
|
||||
const api = new DiscordApi({ token: TOKEN, fetchImpl });
|
||||
await assert.rejects(() => api.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
file: { fileName: 'result.bin', bytes: Buffer.from('result') },
|
||||
}), (error) => error.code === 'artifact-delivery-uncertain');
|
||||
}
|
||||
|
||||
const timeoutApi = new DiscordApi({
|
||||
token: TOKEN,
|
||||
fileUploadTimeoutMs: 10,
|
||||
fetchImpl: async (_url, { signal }) => new Promise((resolve, reject) => {
|
||||
if (signal.aborted) reject(signal.reason);
|
||||
else signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
||||
}),
|
||||
});
|
||||
await assert.rejects(() => timeoutApi.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
file: { fileName: 'result.bin', bytes: Buffer.from('result') },
|
||||
}), (error) => error.code === 'artifact-delivery-uncertain'
|
||||
&& error.cause?.name === 'TimeoutError');
|
||||
|
||||
const caller = new AbortController();
|
||||
const reason = new DOMException('caller stopped', 'AbortError');
|
||||
caller.abort(reason);
|
||||
let calls = 0;
|
||||
const cancelledApi = new DiscordApi({
|
||||
token: TOKEN,
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return jsonResponse({ id: '987654321012345680' });
|
||||
},
|
||||
});
|
||||
await assert.rejects(() => cancelledApi.createFileMessage({
|
||||
channelId: '123456789012345678',
|
||||
file: { fileName: 'result.bin', bytes: Buffer.from('result') },
|
||||
signal: caller.signal,
|
||||
}), (error) => error === reason && error.code !== 'artifact-delivery-uncertain');
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test('Discord controller persists a credential reference and exposes only masked identity', async (t) => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-discord-'));
|
||||
t.after(() => rm(directory, { recursive: true, force: true }));
|
||||
|
|
|
|||
64
test/channels/discord/production.test.mjs
Normal file
64
test/channels/discord/production.test.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { createProductionController } from '../../../plugin-src/host/channels/discord/production.mjs';
|
||||
|
||||
test('Discord production has no per-bot result-file Gate', async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-discord-production-artifacts-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const runtimes = [];
|
||||
let controllerOptions;
|
||||
|
||||
class ConfigStore {
|
||||
async load() { return this; }
|
||||
list() { return []; }
|
||||
}
|
||||
class StateStore {
|
||||
async load() { return this; }
|
||||
}
|
||||
class Harness {
|
||||
stopManagedProcess() {}
|
||||
}
|
||||
class Runtime {
|
||||
constructor(options) { runtimes.push(options); }
|
||||
}
|
||||
class Controller {
|
||||
constructor(options) { controllerOptions = options; }
|
||||
async close() {}
|
||||
}
|
||||
const supervisor = {
|
||||
ready: Promise.resolve(),
|
||||
start() { return this; },
|
||||
async close() {},
|
||||
};
|
||||
const internals = {
|
||||
ConfigStore,
|
||||
StateStore,
|
||||
HarnessClient: Harness,
|
||||
Controller,
|
||||
Runtime,
|
||||
createConnectionSupervisor: () => supervisor,
|
||||
};
|
||||
const ctx = {
|
||||
credentials: {},
|
||||
webServer: { port: 3080 },
|
||||
logger: () => ({ error() {}, warn() {}, info() {}, debug() {} }),
|
||||
};
|
||||
const createRuntime = (botId) => controllerOptions.createRuntime({
|
||||
botId,
|
||||
config: { botId },
|
||||
token: 'host-only',
|
||||
});
|
||||
|
||||
const production = await createProductionController(ctx, {
|
||||
dataDir: join(root, 'default'),
|
||||
}, internals);
|
||||
await createRuntime('discord_enabled');
|
||||
await createRuntime('discord_not_listed');
|
||||
assert.equal(Object.hasOwn(runtimes[0], 'outboundArtifactsEnabled'), false);
|
||||
assert.equal(Object.hasOwn(runtimes[1], 'outboundArtifactsEnabled'), false);
|
||||
await production.close();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue