feat(coding): add model tools and device packages
This commit is contained in:
1 parent
841273b433
commit
664b8823a0
60 files changed
+3847
-2068
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
DevicePackageError,
|
||||
DevicePackageManager,
|
||||
type DevicePackageInstallInput,
|
||||
} from '../../electron/coding-packages/device-package-manager';
|
||||
|
||||
const roots: string[] = [];
|
||||
const NOW = Date.parse('2026-09-02T12:00:00.000Z');
|
||||
|
||||
async function temporaryRoot(name: string): Promise<string> {
|
||||
const root = await mkdtemp(path.join(tmpdir(), `makelore-device-package-${name}-`));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function looseSkill(root: string, id = 'fixture-skill'): Promise<string> {
|
||||
const source = path.join(root, id);
|
||||
await mkdir(source, { recursive: true });
|
||||
await writeFile(path.join(source, 'SKILL.md'), `---\nname: ${id}\ndescription: Fixture\n---\n`, 'utf8');
|
||||
return source;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe('DevicePackageManager', () => {
|
||||
it('previews and commits a loose Skill only after a distinct confirmation turn', async () => {
|
||||
const root = await temporaryRoot('loose-skill');
|
||||
const source = await looseSkill(root);
|
||||
const changed = vi.fn();
|
||||
const manager = new DevicePackageManager({
|
||||
rootDir: path.join(root, 'store'),
|
||||
now: () => NOW,
|
||||
createId: () => 'plan-loose',
|
||||
onGenerationChanged: changed,
|
||||
});
|
||||
|
||||
const preview = await manager.prepare(source, 'turn-prepare');
|
||||
|
||||
expect(preview).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
planId: 'plan-loose',
|
||||
packageId: 'fixture-skill',
|
||||
displayName: 'fixture-skill',
|
||||
kind: 'skill-only',
|
||||
includesExecutableCode: false,
|
||||
scope: 'device-parent-workers',
|
||||
skillEntries: [{ id: 'fixture-skill', entryPath: 'SKILL.md' }],
|
||||
extensionEntries: [],
|
||||
});
|
||||
await expect(manager.commit(preview.planId, true, 'turn-prepare'))
|
||||
.rejects.toMatchObject({ code: 'local_package_confirmation_required' });
|
||||
await expect(manager.commit(preview.planId, false as never, 'turn-confirm'))
|
||||
.rejects.toMatchObject({ code: 'local_package_confirmation_required' });
|
||||
|
||||
const installed = await manager.commit(preview.planId, true, 'turn-confirm');
|
||||
|
||||
expect(installed.generation).toBe(1);
|
||||
expect(installed.packages).toEqual([expect.objectContaining({
|
||||
packageId: 'fixture-skill', enabled: true, confirmedExecutableCode: false,
|
||||
})]);
|
||||
const resources = await manager.resolveEnabledResources();
|
||||
expect(resources).toMatchObject({
|
||||
generation: 1,
|
||||
packageIds: ['fixture-skill'],
|
||||
extensionPaths: [],
|
||||
});
|
||||
expect(resources.skillEntries).toEqual([expect.objectContaining({
|
||||
id: 'fixture-skill',
|
||||
entryPath: 'SKILL.md',
|
||||
})]);
|
||||
const installedSkill = resources.skillEntries[0]!;
|
||||
expect(await readFile(path.join(installedSkill.packageRoot, installedSkill.entryPath), 'utf8'))
|
||||
.toContain('name: fixture-skill');
|
||||
expect(changed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('recognizes Pi extension and mixed package manifests and shows the desktop-permission warning', async () => {
|
||||
const root = await temporaryRoot('pi-manifests');
|
||||
const source = path.join(root, 'mixed-package');
|
||||
await writeJson(path.join(source, 'package.json'), {
|
||||
name: '@example/mixed-package',
|
||||
version: '1.2.3',
|
||||
pi: { extensions: ['./extensions/index.ts'], skills: ['./skills/research/SKILL.md'] },
|
||||
});
|
||||
await mkdir(path.join(source, 'extensions'), { recursive: true });
|
||||
await writeFile(path.join(source, 'extensions/index.ts'), 'export default function fixture() {}\n');
|
||||
await mkdir(path.join(source, 'skills/research'), { recursive: true });
|
||||
await writeFile(path.join(source, 'skills/research/SKILL.md'), '---\nname: research\n---\n');
|
||||
const manager = new DevicePackageManager({
|
||||
rootDir: path.join(root, 'store'),
|
||||
now: () => NOW,
|
||||
createId: () => 'plan-mixed',
|
||||
});
|
||||
|
||||
const preview = await manager.prepare(source, 'turn-a');
|
||||
|
||||
expect(preview).toMatchObject({
|
||||
packageId: 'example.mixed-package',
|
||||
displayName: '@example/mixed-package',
|
||||
resolvedVersion: '1.2.3',
|
||||
kind: 'mixed',
|
||||
includesExecutableCode: true,
|
||||
extensionEntries: ['extensions/index.ts'],
|
||||
skillEntries: [{ id: 'research', entryPath: 'skills/research/SKILL.md' }],
|
||||
});
|
||||
expect(preview.warnings.join(' ')).toContain('完整桌面权限');
|
||||
expect(preview.warnings.join(' ')).toContain('网络');
|
||||
});
|
||||
|
||||
it('runs remote Pi installation with lifecycle scripts disabled and reports declared scripts', async () => {
|
||||
const root = await temporaryRoot('remote');
|
||||
const calls: DevicePackageInstallInput[] = [];
|
||||
const runInstall = vi.fn(async (input: DevicePackageInstallInput) => {
|
||||
calls.push(input);
|
||||
const packageRoot = path.join(input.agentDir, 'npm', 'node_modules', 'pi-web-search');
|
||||
await writeJson(path.join(packageRoot, 'package.json'), {
|
||||
name: 'pi-web-search',
|
||||
version: '2.4.0',
|
||||
scripts: { postinstall: 'node forbidden.js', test: 'ignored test' },
|
||||
pi: { extensions: ['./index.js'] },
|
||||
});
|
||||
await writeFile(path.join(packageRoot, 'index.js'), 'export default function fixture() {}\n');
|
||||
});
|
||||
const manager = new DevicePackageManager({
|
||||
rootDir: path.join(root, 'store'),
|
||||
executablePath: 'Makelore.exe',
|
||||
cliPath: 'pi-cli.js',
|
||||
npmCliPath: 'npm-cli.js',
|
||||
runInstall,
|
||||
now: () => NOW,
|
||||
createId: () => 'plan-npm',
|
||||
});
|
||||
|
||||
const preview = await manager.prepare('npm:pi-web-search@2.4.0', 'turn-a');
|
||||
|
||||
expect(runInstall).toHaveBeenCalledOnce();
|
||||
expect(calls[0]?.source).toBe('npm:pi-web-search@2.4.0');
|
||||
expect(calls[0]?.env).toMatchObject({
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
PI_TELEMETRY: '0',
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
CI: '1',
|
||||
npm_config_ignore_scripts: 'true',
|
||||
npm_config_update_notifier: 'false',
|
||||
});
|
||||
expect(preview).toMatchObject({
|
||||
packageId: 'pi-web-search',
|
||||
resolvedSource: 'npm:pi-web-search@2.4.0',
|
||||
resolvedVersion: '2.4.0',
|
||||
kind: 'pi-extension',
|
||||
ignoredLifecycleScripts: ['postinstall'],
|
||||
});
|
||||
});
|
||||
|
||||
it('increments generation only for durable changes and removes disabled packages', async () => {
|
||||
const root = await temporaryRoot('generation');
|
||||
const source = await looseSkill(root, 'generation-skill');
|
||||
let id = 0;
|
||||
const manager = new DevicePackageManager({
|
||||
rootDir: path.join(root, 'store'),
|
||||
now: () => NOW + id * 1000,
|
||||
createId: () => `plan-${++id}`,
|
||||
});
|
||||
const preview = await manager.prepare(source, 'turn-a');
|
||||
await manager.commit(preview.planId, true, 'turn-b');
|
||||
|
||||
expect((await manager.setEnabled('generation-skill', true)).generation).toBe(1);
|
||||
expect((await manager.setEnabled('generation-skill', false)).generation).toBe(2);
|
||||
expect((await manager.resolveEnabledResources()).packageIds).toEqual([]);
|
||||
expect((await manager.setEnabled('generation-skill', false)).generation).toBe(2);
|
||||
expect((await manager.uninstall('generation-skill')).generation).toBe(3);
|
||||
expect((await manager.uninstall('generation-skill')).generation).toBe(3);
|
||||
expect((await manager.list()).packages).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects escaping and ambiguous manifest entries', async () => {
|
||||
const root = await temporaryRoot('invalid');
|
||||
const source = path.join(root, 'bad-package');
|
||||
await writeJson(path.join(source, 'package.json'), {
|
||||
name: 'bad-package', version: '1.0.0', pi: { extensions: ['../outside.js'] },
|
||||
});
|
||||
await writeFile(path.join(root, 'outside.js'), 'export default function bad() {}\n');
|
||||
const manager = new DevicePackageManager({ rootDir: path.join(root, 'store') });
|
||||
|
||||
await expect(manager.prepare(source, 'turn-a')).rejects.toBeInstanceOf(DevicePackageError);
|
||||
await expect(manager.prepare(source, 'turn-a'))
|
||||
.rejects.toMatchObject({ code: 'local_package_manifest_invalid' });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user