Read the current player's captions after one click and write a timestamped note. Summaries use the workspace default model when the token includes ai:generate.
100 lines
7.3 KiB
JavaScript
100 lines
7.3 KiB
JavaScript
import { expect, test } from 'bun:test';
|
|
import { Hono } from 'hono';
|
|
import { registerPluginCapabilityRoutes } from './plugin-capability-routes';
|
|
|
|
function fixture({ kind = 'user', demo = false, fetch = async () => new Response('public data'), generate = async input => ({ text: input.prompt.toUpperCase() }), loadCredentials } = {}) {
|
|
const app = new Hono();
|
|
app.use('*', async (c, next) => { if (kind) c.set('auth', { kind, workspaceId: 'workspace' }); await next(); });
|
|
registerPluginCapabilityRoutes(app, { isDemoMode: () => demo, aiStatus: async () => ({ configured: true, modelName: 'Test model' }), generate, loadCredentials });
|
|
const env = { publicNetworkFetch: fetch };
|
|
return (path, body, signal) => app.request(`/api/v1/plugins/${path}`, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal } : {}, env);
|
|
}
|
|
|
|
test('generic capabilities reject agent tokens, unauthenticated access and public demo calls', async () => {
|
|
for (const options of [{ kind: 'agent' }, { kind: null }, { demo: true }]) {
|
|
const call = fixture({
|
|
...options,
|
|
fetch: () => { throw new Error('must not run'); },
|
|
loadCredentials: async () => { throw new Error('credentials must stay on the server'); },
|
|
});
|
|
expect((await call('network/fetch', { url: 'https://example.org' })).status).toBe(403);
|
|
expect((await call('ai/status')).status).toBe(403);
|
|
const prepare = await call('ai/generate/prepare', { system: '', prompt: 'caption text' });
|
|
expect(prepare.status).toBe(403);
|
|
expect(await prepare.text()).not.toContain('apiKey');
|
|
}
|
|
});
|
|
|
|
test('public egress refuses private targets, credentials, unsupported methods and sensitive headers before transport', async () => {
|
|
let calls = 0; const call = fixture({ fetch: async () => { calls++; return new Response('bad'); } });
|
|
for (const url of ['http://example.org', 'https://127.0.0.1', 'https://2130706433', 'https://[::1]', 'https://user:password@example.org', 'https://example.org:8443', 'https://metadata.google.internal', 'https://localhost']) expect((await call('network/fetch', { url })).status).toBe(400);
|
|
for (const headers of [{ Authorization: 'Bearer dummy' }, { Cookie: 'dummy' }, { Host: 'localhost' }, { Accept: 'text/plain\r\nx-test: dummy' }]) expect((await call('network/fetch', { url: 'https://example.org', headers })).status).toBe(400);
|
|
expect((await call('network/fetch', { url: 'https://example.org', method: 'POST' })).status).toBe(400);
|
|
expect(calls).toBe(0);
|
|
});
|
|
|
|
test('public transport preserves upstream status and useful headers without following redirects or returning cookies', async () => {
|
|
let calls = 0;
|
|
const call = fixture({ fetch: async (url, init) => {
|
|
calls++; expect(url).toBe('https://example.org/feed'); expect(init.redirect).toBe('manual'); expect(init.credentials).toBe('omit'); expect(init.headers.accept).toBe('application/rss+xml');
|
|
return new Response('moved', { status: 302, headers: { Location: 'https://127.0.0.1/private', 'Set-Cookie': 'secret=not-for-plugin', 'X-Internal': 'hidden' } });
|
|
} });
|
|
const response = await call('network/fetch', { url: 'https://example.org/feed', headers: { Accept: 'application/rss+xml' } });
|
|
expect(response.status).toBe(200); expect(response.headers.get('x-edgeever-upstream-status')).toBe('302'); expect(await response.text()).toBe('moved'); expect(response.headers.get('x-edgeever-upstream-header-location')).toBe('https://127.0.0.1/private'); expect(response.headers.get('x-edgeever-upstream-header-set-cookie')).toBeNull(); expect(response.headers.get('x-edgeever-upstream-header-x-internal')).toBeNull(); expect(calls).toBe(1);
|
|
});
|
|
|
|
test('oversized and cancelled public responses fail without exposing upstream error details', async () => {
|
|
const large = fixture({ fetch: async () => new Response(new Uint8Array(2_000_001)) });
|
|
expect((await large('network/fetch', { url: 'https://example.org' })).status).toBe(502);
|
|
const cancelled = fixture({ fetch: async (_url, init) => { init.signal.throwIfAborted(); return new Response('unexpected'); } });
|
|
const controller = new AbortController(); controller.abort();
|
|
expect((await cancelled('network/fetch', { url: 'https://example.org' }, controller.signal)).status).toBe(502);
|
|
});
|
|
|
|
test('generic AI prepare returns credentials without invoking the provider', async () => {
|
|
let generated = 0;
|
|
const call = fixture({
|
|
generate: async () => { generated++; return { text: 'nope' }; },
|
|
loadCredentials: async () => ({ provider: 'openai-compatible', baseUrl: 'https://api.example/v1', apiKey: 'plugin-key', modelId: 'model-a' }),
|
|
});
|
|
const response = await call('ai/generate/prepare', { system: 'Translate', prompt: 'hello', maxOutputTokens: 100 });
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toMatchObject({
|
|
apiKey: 'plugin-key', system: 'Translate', prompt: 'hello', maxOutputTokens: 100,
|
|
});
|
|
expect(generated).toBe(0);
|
|
});
|
|
|
|
test('generic AI validates ordinary prompts, returns text, and redacts provider errors', async () => {
|
|
const call = fixture();
|
|
expect(await (await call('ai/status')).json()).toEqual({ configured: true, modelName: 'Test model' });
|
|
expect(await (await call('ai/generate', { system: 'Translate', prompt: 'hello', maxOutputTokens: 100 })).json()).toEqual({ text: 'HELLO' });
|
|
expect((await call('ai/generate', { system: '', prompt: 'hello', source: 'github' })).status).toBe(400);
|
|
expect((await call('ai/generate', { system: '', prompt: 'x'.repeat(90001) })).status).toBe(400);
|
|
const failed = fixture({ generate: async () => { throw new Error('Bearer dummy-provider-secret'); } });
|
|
const response = await failed('ai/generate', { system: '', prompt: 'hello' }); expect(response.status).toBe(502); expect(await response.text()).not.toContain('dummy-provider-secret');
|
|
});
|
|
|
|
test('generic AI leaves output limits to the provider while requiring a positive integer', async () => {
|
|
const call = fixture({
|
|
generate: async input => ({ text: String(input.maxOutputTokens) }),
|
|
loadCredentials: async () => ({ provider: 'openai-compatible', baseUrl: 'https://api.example/v1', apiKey: 'plugin-key', modelId: 'model-a' }),
|
|
});
|
|
const input = { system: '', prompt: 'hello', maxOutputTokens: 1000000 };
|
|
expect(await (await call('ai/generate', input)).json()).toEqual({ text: '1000000' });
|
|
expect((await call('ai/generate/prepare', input)).status).toBe(200);
|
|
for (const maxOutputTokens of [0, -1, 1.5, '100']) {
|
|
expect((await call('ai/generate', { ...input, maxOutputTokens })).status).toBe(400);
|
|
expect((await call('ai/generate/prepare', { ...input, maxOutputTokens })).status).toBe(400);
|
|
}
|
|
});
|
|
|
|
test('concurrent requests are bounded and released after completion', async () => {
|
|
let started = 0; let release; const gate = new Promise(resolve => { release = resolve; });
|
|
const call = fixture({ fetch: async () => { started++; await gate; return new Response('ok'); } });
|
|
const pending = Array.from({ length: 4 }, () => call('network/fetch', { url: 'https://example.org' }));
|
|
for (let i = 0; i < 100 && started < 4; i++) await new Promise(resolve => setTimeout(resolve, 1));
|
|
expect(started).toBe(4); expect((await call('network/fetch', { url: 'https://example.org' })).status).toBe(429);
|
|
release(); await Promise.all(pending); expect((await call('network/fetch', { url: 'https://example.org' })).status).toBe(200);
|
|
});
|