feat: plugins v1.5

This commit is contained in:
2026-04-29 01:14:30 +02:00
parent 6920f93b41
commit eabbc08896
59 changed files with 2197 additions and 352 deletions

View File

@@ -28,22 +28,22 @@ test.describe('Plugin API multi-user runtime', () => {
test('runs chat, embed, soundboard, and profile APIs between two users', async ({ createClient }) => {
const scenario = await createPluginApiScenario(createClient);
await test.step('Install and activate the plugin for Bob as the embed/soundboard receiver', async () => {
await installGrantAndActivatePlugin(scenario.bob.page);
await closeSettingsModal(scenario.bob.page);
await expect(soundboardComposerButton(scenario.bob.page)).toBeVisible({ timeout: 20_000 });
await expect(scenario.bob.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 });
await expect(scenario.bob.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin');
});
await test.step('Install and activate the plugin for Alice as the API driver', async () => {
await installGrantAndActivatePlugin(scenario.alice.page);
await test.step('Install the server plugin as Alice', async () => {
await installGrantAndActivatePlugin(scenario.alice.page, true);
await closeSettingsModal(scenario.alice.page);
await expect(soundboardComposerButton(scenario.alice.page)).toBeVisible({ timeout: 20_000 });
await expect(scenario.alice.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 });
await expect(scenario.alice.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin');
});
await test.step('Activate the server plugin for Bob as the embed/soundboard receiver', async () => {
await installGrantAndActivatePlugin(scenario.bob.page, false);
await closeSettingsModal(scenario.bob.page);
await expect(soundboardComposerButton(scenario.bob.page)).toBeVisible({ timeout: 20_000 });
await expect(scenario.bob.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 });
await expect(scenario.bob.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin');
});
await test.step('Alice opens the plugin soundboard modal and plays a sound to voice', async () => {
await soundboardComposerButton(scenario.alice.page).click();
await expect(scenario.alice.page.getByRole('dialog', { name: SOUND_BOARD_LABEL })).toBeVisible({ timeout: 20_000 });
@@ -140,15 +140,21 @@ async function registerUser(page: Page, username: string, displayName: string):
await expect(page).toHaveURL(/\/search/, { timeout: 30_000 });
}
async function installGrantAndActivatePlugin(page: Page): Promise<void> {
async function installGrantAndActivatePlugin(page: Page, installFromStore: boolean): Promise<void> {
await page.getByRole('button', { name: 'Plugins' }).click();
await expect(page).toHaveURL(/\/plugin-store/, { timeout: 20_000 });
await expect(page.getByTestId('plugin-store-page')).toBeVisible({ timeout: 20_000 });
await page.getByPlaceholder('https://example.com/plugins.json').fill(PLUGIN_SOURCE_URL);
await page.getByRole('button', { name: 'Add Source' }).click();
await expect(page.getByRole('heading', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 });
await page.getByRole('button', { exact: true, name: 'Install' }).click();
await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('Installed')).toBeVisible({ timeout: 20_000 });
if (installFromStore) {
await page.getByLabel('Plugin source manifest URL').fill(PLUGIN_SOURCE_URL);
await page.getByRole('button', { name: 'Add Source' }).click();
await expect(page.getByRole('heading', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 });
await page.getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ }).click();
await expect(page.getByRole('dialog', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 10_000 });
await page.getByRole('button', { name: 'Install and Activate' }).click();
await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('Installed')).toBeVisible({ timeout: 20_000 });
}
await page.getByRole('button', { name: 'Manage Plugins' }).click();
await expect(page.getByTestId('plugin-manager')).toBeVisible({ timeout: 20_000 });
await expect(page.locator('article', { hasText: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 });

View File

@@ -30,20 +30,25 @@ test.describe('Plugin manager UI', () => {
});
await test.step('Install fixture plugin from source manifest', async () => {
await page.getByPlaceholder('https://example.com/plugins.json').fill('http://localhost:4200/plugins/e2e-plugin-source.json');
await page.getByLabel('Plugin source manifest URL').fill('http://localhost:4200/plugins/e2e-plugin-source.json');
await page.getByRole('button', { name: 'Add Source' }).click();
await expect(page.getByRole('heading', { name: 'E2E All API Plugin' })).toBeVisible({ timeout: 15_000 });
await page.getByRole('button', { name: 'Readme' }).click();
await expect(page.getByText('Fixture plugin for Playwright coverage.')).toBeVisible({ timeout: 10_000 });
await page.getByRole('button', { exact: true, name: 'Install' }).click();
await page.getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ }).click();
const installDialog = page.getByRole('dialog', { name: 'E2E All API Plugin' });
await expect(installDialog).toBeVisible({ timeout: 10_000 });
await expect(installDialog.getByText('Install to server', { exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Install and Activate' }).click();
await expect(page.locator('article', { hasText: 'E2E All API Plugin' }).getByText('Installed')).toBeVisible({ timeout: 10_000 });
});
await test.step('Open plugin manager from the store page', async () => {
await page.getByRole('button', { name: 'Manage Plugins' }).click();
await expect(page.getByTestId('plugin-manager')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('plugin-manager').getByRole('heading', { name: 'Plugins' })).toBeVisible();
await expect(page.getByText('Development Plugin')).toBeVisible();
await expect(page.getByTestId('plugin-manager').getByRole('heading', { name: 'Server plugins' })).toBeVisible();
await expect(page.getByText('E2E All API Plugin')).toBeVisible();
});
await test.step('Grant capabilities and activate runtime', async () => {

View File

@@ -34,21 +34,6 @@ interface PluginEventDefinitionResponse {
};
}
interface PluginDataResponse {
record: {
key: string;
ownerId?: string;
pluginId: string;
schemaVersion: number;
scope: string;
value: unknown;
};
}
interface PluginDataListResponse {
records: PluginDataResponse['record'][];
}
interface PluginSnapshotResponse {
eventDefinitions: PluginEventDefinitionResponse['eventDefinition'][];
requirements: PluginRequirementResponse['requirement'][];
@@ -136,8 +121,8 @@ test.describe('Plugin support API', () => {
expect(snapshot.eventDefinitions.map((entry) => entry.eventName).sort()).toEqual([TEST_PLUGIN_P2P_EVENT, TEST_PLUGIN_RELAY_EVENT]);
});
await test.step('Plugin data API stores, lists, and deletes server scoped data', async () => {
const stored = await expectJson<PluginDataResponse>(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
await test.step('Plugin data API refuses arbitrary server persistence', async () => {
const stored = await expectJson<{ errorCode: string }>(await request.put(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
data: {
actorUserId: OWNER_USER_ID,
schemaVersion: 1,
@@ -147,49 +132,28 @@ test.describe('Plugin support API', () => {
pluginVersion: manifest.version
}
}
}));
}), 410);
expect(stored.record).toEqual(expect.objectContaining({
key: 'settings',
pluginId: TEST_PLUGIN_ID,
schemaVersion: 1,
scope: 'server',
value: {
enabled: true,
pluginVersion: manifest.version
}
}));
expect(stored.errorCode).toBe('PLUGIN_DATA_DISABLED');
const listed = await expectJson<PluginDataListResponse>(await request.get(`${pluginsApi}/${TEST_PLUGIN_ID}/data`, {
const listed = await expectJson<{ errorCode: string }>(await request.get(`${pluginsApi}/${TEST_PLUGIN_ID}/data`, {
params: {
key: 'settings',
scope: 'server',
userId: OWNER_USER_ID
}
}));
}), 410);
expect(listed.records).toHaveLength(1);
expect(listed.records[0]?.value).toEqual({
enabled: true,
pluginVersion: manifest.version
});
expect(listed.errorCode).toBe('PLUGIN_DATA_DISABLED');
await expectJson<{ ok: boolean }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
const afterDelete = await expectJson<{ errorCode: string }>(await request.delete(`${pluginsApi}/${TEST_PLUGIN_ID}/data/settings`, {
data: {
actorUserId: OWNER_USER_ID,
scope: 'server'
}
}));
}), 410);
const afterDelete = await expectJson<PluginDataListResponse>(await request.get(`${pluginsApi}/${TEST_PLUGIN_ID}/data`, {
params: {
key: 'settings',
scope: 'server',
userId: OWNER_USER_ID
}
}));
expect(afterDelete.records).toEqual([]);
expect(afterDelete.errorCode).toBe('PLUGIN_DATA_DISABLED');
});
await test.step('WebSocket plugin API sends snapshots, relays server events, and rejects p2p relays', async () => {