Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/desktop/e2e/module-hub.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { expect, test } from './fixtures';

test('Module Hub switches all four leaves and opens scheduled creation once', async ({
window: page,
}) => {
const expand = page.getByRole('button', { name: '展开侧边栏' });
if (await expand.isVisible()) await expand.click();
const sidebar = page.getByRole('navigation', { name: '任务列表' });

await sidebar.getByRole('button', { name: '扩展', exact: true }).click();
await expect(page.locator('[data-module="skills"]')).toBeVisible();
const extensions = page.getByRole('navigation', { name: /扩展内容/ });
await extensions.getByRole('button', { name: 'MCP', exact: true }).click();
await expect(
extensions.getByRole('button', { name: 'MCP', exact: true }),
).toHaveAttribute('aria-current', 'page');

await sidebar.getByRole('button', { name: /定时任务/ }).click();
await expect(page.locator('[data-module="scheduled-tasks"]')).toBeVisible();
const automations = page.getByRole('navigation', { name: /定时任务内容/ });
await automations.getByRole('button', { name: '每日回顾', exact: true }).click();
await expect(page.locator('[data-module="daily-review"]')).toBeVisible();

await page.keyboard.press(process.platform === 'darwin' ? 'Meta+k' : 'Control+k');
const palette = page.getByRole('dialog', { name: '命令面板' });
await expect(palette).toBeVisible();
await palette.getByRole('option', { name: /新建定时任务/ }).click();

const createDialog = page.getByRole('dialog', { name: '新建定时任务' });
await expect(createDialog).toBeVisible();
await expect(createDialog).toHaveCount(1);
await expect(page.locator('[data-module="scheduled-tasks"]')).toBeVisible();
});
138 changes: 138 additions & 0 deletions apps/desktop/src/main/__tests__/module-hub-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';

const desktopRoot = resolve(fileURLToPath(new URL('../../../', import.meta.url)));
const featureRoot = join(
desktopRoot,
'src',
'renderer',
'features',
'module-hub',
);

function sourceFiles(root: string): string[] {
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
const path = join(root, entry.name);
if (entry.isDirectory()) return sourceFiles(path);
return /\.(?:ts|tsx|md)$/.test(entry.name) ? [path] : [];
});
}

describe('Module Hub feature boundary', () => {
it('keeps Desktop globals and shell/process dependencies outside production feature code', () => {
const violations: string[] = [];
for (const path of sourceFiles(featureRoot)) {
if (path.endsWith(`${join('', 'testing.ts')}`)) continue;
const source = readFileSync(path, 'utf8');
const name = relative(desktopRoot, path);
if (!path.endsWith('.md')) {
if (source.includes('window.maka')) violations.push(`${name}: window.maka`);
if (source.includes('navigator.')) violations.push(`${name}: navigator`);
}
for (const match of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
const imported = match[1] ?? '';
if (
imported.includes('app-shell') ||
imported.includes('/preload/') ||
imported.includes('/main/')
) {
violations.push(`${name}: ${imported}`);
}
}
}
assert.deepEqual(violations, []);
});

it('is consumed outside the feature only through index or testing', () => {
const allowed = /\/features\/module-hub\/(?:index|testing)(?:\.js)?$/;
const violations: string[] = [];
for (const root of [join(desktopRoot, 'src'), join(desktopRoot, 'stories')]) {
for (const path of sourceFiles(root)) {
if (path.startsWith(featureRoot)) continue;
const source = readFileSync(path, 'utf8');
for (const match of source.matchAll(
/from\s+['"]([^'"]*features\/module-hub[^'"]*)['"]/g,
)) {
const imported = (match[1] ?? '').replace(/\\/g, '/');
const explicitEntry = imported.endsWith('/features/module-hub')
? `${imported}/index`
: imported;
if (!allowed.test(explicitEntry)) {
violations.push(`${relative(desktopRoot, path)}: ${imported}`);
}
}
}
}
assert.deepEqual(violations, []);
});

it('keeps fakes out of the production entry', () => {
const productionEntry = readFileSync(join(featureRoot, 'index.ts'), 'utf8');
assert.equal(productionEntry.includes('createFakeModuleHub'), false);
assert.equal(productionEntry.includes("from './testing"), false);
});

it('keeps module data, pages, nonce, bridges, and subscriptions out of AppShell', () => {
const appShell = readFileSync(
join(desktopRoot, 'src', 'renderer', 'app-shell.tsx'),
'utf8',
);
for (const forbidden of [
'useAppShellModuleData',
'useKeepSystemAwake',
'createAppShellDailyReviewBridge',
'createAppShellDailyReviewActions',
'scheduledTaskCreateRequestNonce',
'ModuleHubSelector',
'<SkillsPage',
'<ScheduledTasksPage',
'<DailyReviewPage',
'<McpPage',
'refreshScheduledTasks',
'refreshManagedSkillSources',
'refreshBundledSkillCatalog',
]) {
assert.equal(appShell.includes(forbidden), false, forbidden);
}
assert.equal(appShell.includes('useModuleHubController({'), true);
assert.equal(appShell.includes('<ModuleHubHost model={moduleHub.host} />'), true);

const effects = readFileSync(
join(desktopRoot, 'src', 'renderer', 'app-shell-effects.ts'),
'utf8',
);
assert.equal(effects.includes('window.maka.scheduledTasks'), false);

const commands = readFileSync(
join(desktopRoot, 'src', 'renderer', 'app-shell-command-actions.ts'),
'utf8',
);
assert.equal(commands.includes('dailyReviewBridge'), false);
assert.equal(commands.includes('saveDailyReviewMarkdown'), false);
assert.equal(commands.includes('copyTodayDailyReview()'), true);
assert.equal(commands.includes('pasteTodayDailyReview()'), true);
assert.equal(commands.includes('saveTodayDailyReview()'), true);
});
});
Loading