收口 Makelore 客户端变更
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-12 22:35:06 +08:00
parent 253bad8b40
commit f4113a872f
232 changed files with 4617 additions and 20121 deletions

View File

@@ -6,7 +6,6 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs';
@@ -16,10 +15,8 @@ import type { ChildProcess, SpawnOptionsWithoutStdio } from 'node:child_process'
import { afterEach, describe, expect, it, vi } from 'vitest';
import { OpencodeManager } from '@electron/opencode/manager';
import {
ensureBundledSuperpowersPlugin,
resolveBundledAgentBrowserPluginPath,
resolveBundledSuperpowersDir,
} from '@electron/opencode/superpowers';
} from '@electron/opencode/course-skills';
import { logger } from '@electron/utils/logger';
type SpawnCall = {
@@ -50,41 +47,12 @@ function createSpawnHarness() {
return { children, calls, spawn };
}
function createBundledSuperpowersFixture(): string {
const sourceDir = mkdtempSync(join(tmpdir(), 'niancode-superpowers-source-'));
writeFileSync(
join(sourceDir, 'package.json'),
JSON.stringify({ name: 'superpowers', version: '5.0.7' }),
);
writeFileSync(join(sourceDir, 'LICENSE'), 'source-license');
const pluginDir = join(sourceDir, '.opencode', 'plugins');
const skillDir = join(sourceDir, 'skills', 'using-superpowers');
mkdirSync(pluginDir, { recursive: true });
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(pluginDir, 'superpowers.js'),
'export const SuperpowersPlugin = async () => ({});\n',
);
writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: using-superpowers\n---\n');
return sourceDir;
}
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('OpencodeManager', () => {
it('resolves the packaged Superpowers source without depending on the managed destination name', () => {
const resourcesPath = 'C:\\Program Files\\NianCode\\resources';
expect(resolveBundledSuperpowersDir({
isPackaged: true,
resourcesPath,
appPath: 'C:\\Program Files\\NianCode\\resources\\app.asar',
})).toBe(join(resourcesPath, 'resources', 'skills', 'superpowers'));
});
it('resolves the bundled Agent Browser plugin from the course skills bundle', () => {
const resourcesPath = 'C:\\Program Files\\Makelore\\resources';
@@ -762,216 +730,6 @@ describe('OpencodeManager', () => {
}
});
it('installs bundled Superpowers into the managed opencode config directory before spawning', async () => {
const userDataDir = mkdtempSync(join(tmpdir(), 'niancode-opencode-manager-'));
const bundledSuperpowersDir = createBundledSuperpowersFixture();
try {
const { children, calls, spawn } = createSpawnHarness();
const manager = new OpencodeManager({
port: 4328,
binPath: '/opt/opencode',
userDataDir,
bundledSuperpowersDir,
runtimeConfigProvider: () => ({ config: {}, env: {} }),
spawn,
});
const startPromise = manager.start();
children[0].stdout.emit(
'data',
Buffer.from('opencode server listening on http://127.0.0.1:4328\n'),
);
await startPromise;
const opencodeDir = join(userDataDir, 'opencode');
const managedConfigDir = join(opencodeDir, 'niancode-config');
const installedRepoDir = join(managedConfigDir, 'superpowers-bundles', '5.0.7');
const wrapperPath = join(managedConfigDir, 'plugins', 'superpowers-niancode.js');
expect(calls[0].options.env).toMatchObject({
OPENCODE_CONFIG_DIR: managedConfigDir,
});
expect(existsSync(join(installedRepoDir, 'skills', 'using-superpowers', 'SKILL.md'))).toBe(true);
expect(existsSync(join(installedRepoDir, '.opencode', 'plugins', 'superpowers.js'))).toBe(true);
expect(readFileSync(wrapperPath, 'utf8')).toContain(
"export { SuperpowersPlugin } from '../superpowers-bundles/5.0.7/.opencode/plugins/superpowers.js';",
);
expect(JSON.parse(readFileSync(
join(managedConfigDir, 'superpowers-active.json'),
'utf8',
))).toEqual({ version: '5.0.7', directory: '5.0.7' });
} finally {
rmSync(userDataDir, { recursive: true, force: true });
rmSync(bundledSuperpowersDir, { recursive: true, force: true });
}
});
it('migrates the wrapper without touching an unusable legacy destination under a non-ASCII user path', () => {
const managedRoot = mkdtempSync(join(tmpdir(), 'niancode-managed-config-'));
const managedConfigDir = join(
managedRoot,
'卢欢',
'AppData',
'Roaming',
'niancode',
'opencode',
'niancode-config',
);
mkdirSync(managedConfigDir, { recursive: true });
const bundledSuperpowersDir = createBundledSuperpowersFixture();
try {
const legacyPath = join(managedConfigDir, 'superpowers');
writeFileSync(legacyPath, 'legacy-path-must-remain-untouched');
const wrapperPath = join(managedConfigDir, 'plugins', 'superpowers-niancode.js');
mkdirSync(join(managedConfigDir, 'plugins'), { recursive: true });
writeFileSync(
wrapperPath,
"export { SuperpowersPlugin } from '../superpowers/.opencode/plugins/superpowers.js';\n",
);
expect(ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: bundledSuperpowersDir,
})).toBe(true);
expect(readFileSync(legacyPath, 'utf8')).toBe('legacy-path-must-remain-untouched');
expect(readFileSync(wrapperPath, 'utf8')).not.toContain('../superpowers/.opencode');
expect(readFileSync(wrapperPath, 'utf8')).toContain('../superpowers-bundles/5.0.7/.opencode');
expect(existsSync(join(
managedConfigDir,
'superpowers-bundles',
'5.0.7',
'skills',
'using-superpowers',
'SKILL.md',
))).toBe(true);
} finally {
rmSync(managedRoot, { recursive: true, force: true });
rmSync(bundledSuperpowersDir, { recursive: true, force: true });
}
});
it('reuses an installed Superpowers version without overwriting its files', () => {
const managedConfigDir = mkdtempSync(join(tmpdir(), 'niancode-managed-config-'));
const bundledSuperpowersDir = createBundledSuperpowersFixture();
try {
expect(ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: bundledSuperpowersDir,
})).toBe(true);
const installedLicense = join(
managedConfigDir,
'superpowers-bundles',
'5.0.7',
'LICENSE',
);
writeFileSync(installedLicense, 'installed-copy-must-not-be-overwritten');
expect(ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: bundledSuperpowersDir,
})).toBe(true);
expect(readFileSync(installedLicense, 'utf8')).toBe('installed-copy-must-not-be-overwritten');
} finally {
rmSync(managedConfigDir, { recursive: true, force: true });
rmSync(bundledSuperpowersDir, { recursive: true, force: true });
}
});
it('cleans an incomplete Superpowers staging directory', () => {
const managedConfigDir = mkdtempSync(join(tmpdir(), 'niancode-managed-config-'));
const bundledSuperpowersDir = createBundledSuperpowersFixture();
try {
rmSync(join(
bundledSuperpowersDir,
'skills',
'using-superpowers',
'SKILL.md',
));
expect(() => ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: bundledSuperpowersDir,
})).toThrow('Bundled Superpowers staging copy is incomplete');
const bundlesDir = join(managedConfigDir, 'superpowers-bundles');
expect(readdirSync(bundlesDir).some((entry) => entry.startsWith('.staging-'))).toBe(false);
} finally {
rmSync(managedConfigDir, { recursive: true, force: true });
rmSync(bundledSuperpowersDir, { recursive: true, force: true });
}
});
it('keeps the active bundle when a newer Superpowers bundle is incomplete', () => {
const managedConfigDir = mkdtempSync(join(tmpdir(), 'niancode-managed-config-'));
const installedSourceDir = createBundledSuperpowersFixture();
const brokenSourceDir = createBundledSuperpowersFixture();
try {
expect(ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: installedSourceDir,
})).toBe(true);
const manifestPath = join(managedConfigDir, 'superpowers-active.json');
const originalManifest = readFileSync(manifestPath, 'utf8');
writeFileSync(
join(brokenSourceDir, 'package.json'),
JSON.stringify({ name: 'superpowers', version: '5.0.8' }),
);
rmSync(join(brokenSourceDir, 'skills', 'using-superpowers', 'SKILL.md'));
expect(() => ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: brokenSourceDir,
})).toThrow('Bundled Superpowers staging copy is incomplete');
expect(readFileSync(manifestPath, 'utf8')).toBe(originalManifest);
expect(existsSync(join(
managedConfigDir,
'superpowers-bundles',
'5.0.7',
'.opencode',
'plugins',
'superpowers.js',
))).toBe(true);
} finally {
rmSync(managedConfigDir, { recursive: true, force: true });
rmSync(installedSourceDir, { recursive: true, force: true });
rmSync(brokenSourceDir, { recursive: true, force: true });
}
});
it('uses a fresh bundle directory when the preferred version path is unusable', () => {
const managedConfigDir = mkdtempSync(join(tmpdir(), 'niancode-managed-config-'));
const bundledSuperpowersDir = createBundledSuperpowersFixture();
try {
const bundlesDir = join(managedConfigDir, 'superpowers-bundles');
mkdirSync(bundlesDir, { recursive: true });
writeFileSync(join(bundlesDir, '5.0.7'), 'unusable-preferred-path');
expect(ensureBundledSuperpowersPlugin({
managedConfigDir,
sourceDir: bundledSuperpowersDir,
})).toBe(true);
const manifest = JSON.parse(readFileSync(
join(managedConfigDir, 'superpowers-active.json'),
'utf8',
)) as { version: string; directory: string };
expect(manifest.version).toBe('5.0.7');
expect(manifest.directory).toMatch(/^5\.0\.7-/);
expect(existsSync(join(
bundlesDir,
manifest.directory,
'.opencode',
'plugins',
'superpowers.js',
))).toBe(true);
} finally {
rmSync(managedConfigDir, { recursive: true, force: true });
rmSync(bundledSuperpowersDir, { recursive: true, force: true });
}
});
it('installs bundled course skills into the managed opencode config directory before spawning', async () => {
const userDataDir = mkdtempSync(join(tmpdir(), 'niancode-opencode-manager-'));
const bundledCourseSkillsDir = mkdtempSync(join(tmpdir(), 'niancode-course-skills-source-'));
@@ -1059,6 +817,45 @@ describe('OpencodeManager', () => {
}
});
it('removes legacy Superpowers runtime artifacts before spawning', async () => {
const userDataDir = mkdtempSync(join(tmpdir(), 'niancode-opencode-manager-'));
const managedConfigDir = join(userDataDir, 'opencode', 'niancode-config');
const legacyPluginPath = join(managedConfigDir, 'plugins', 'superpowers-niancode.js');
const legacyPluginAliasPath = join(managedConfigDir, 'plugins', 'superpowers.js');
const legacyManifestPath = join(managedConfigDir, 'superpowers-active.json');
const legacyBundlePath = join(managedConfigDir, 'superpowers-bundles', '5.0.7', 'skills');
try {
mkdirSync(join(managedConfigDir, 'plugins'), { recursive: true });
mkdirSync(legacyBundlePath, { recursive: true });
writeFileSync(legacyPluginPath, 'legacy plugin');
writeFileSync(legacyPluginAliasPath, 'legacy plugin alias');
writeFileSync(legacyManifestPath, '{}');
const { children, spawn } = createSpawnHarness();
const manager = new OpencodeManager({
port: 4338,
binPath: '/opt/opencode',
userDataDir,
runtimeConfigProvider: () => ({ config: {}, env: {} }),
spawn,
});
const startPromise = manager.start();
children[0].stdout.emit(
'data',
Buffer.from('opencode server listening on http://127.0.0.1:4338\n'),
);
await startPromise;
expect(existsSync(legacyPluginPath)).toBe(false);
expect(existsSync(legacyPluginAliasPath)).toBe(false);
expect(existsSync(legacyManifestPath)).toBe(false);
expect(existsSync(join(managedConfigDir, 'superpowers-bundles'))).toBe(false);
} finally {
rmSync(userDataDir, { recursive: true, force: true });
}
});
it('removes retired bundled skills from the managed config before spawning', async () => {
const userDataDir = mkdtempSync(join(tmpdir(), 'niancode-opencode-manager-'));
const bundledCourseSkillsDir = mkdtempSync(join(tmpdir(), 'niancode-course-skills-source-'));