import { mkdtemp, mkdir, 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 YAML from 'yaml'; import { CodingCapabilityRegistryImpl } from '@electron/coding-plugins/registry'; import type { PiProviderSelection } from '@electron/coding-runtime/pi/provider-config'; import { buildPiManagedInputArgs, getPiManagedPaths, MAKELORE_DEFAULT_LANGUAGE_PROMPT, materializePiAgentResources, resolveBundledCodingSkillsDir, resolveExplicitCodingSkillPaths, } from '@electron/coding-runtime/pi/resource-loader'; import type { PluginPolicyClientState } from '@electron/services/plugin-policy-client'; import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins'; import type { EffectivePluginSnapshot } from '../../electron/coding-plugins/effective-resolver'; const temporaryRoots: string[] = []; afterEach(async () => { await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); async function fixtureRoot(): Promise<{ root: string; userDataDir: string; projectDir: string; skillsDir: string; }> { const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-resources-')); temporaryRoots.push(root); const userDataDir = path.join(root, 'user-data'); const projectDir = path.join(root, 'project'); const skillsDir = path.join(root, 'bundled-skills'); await Promise.all([ mkdir(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill'), { recursive: true }), mkdir(path.join(projectDir, '.agents', 'skills', 'untrusted-agent-skill'), { recursive: true }), mkdir(path.join(skillsDir, 'grilling'), { recursive: true }), mkdir(path.join(skillsDir, 'agent-browser'), { recursive: true }), mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service'), { recursive: true }), mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource'), { recursive: true }), ]); await Promise.all([ writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'), writeFile(path.join(projectDir, '.agents', 'skills', 'untrusted-agent-skill', 'SKILL.md'), 'untrusted', 'utf8'), writeFile(path.join(skillsDir, 'grilling', 'SKILL.md'), '---\nname: grilling\n---\n', 'utf8'), writeFile(path.join(skillsDir, 'agent-browser', 'SKILL.md'), '---\nname: agent-browser\n---\n', 'utf8'), writeFile( path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'), '---\nname: data-service\n---\n', 'utf8', ), writeFile( path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'), '---\nname: game-resource\n---\n', 'utf8', ), ]); return { root, userDataDir, projectDir, skillsDir }; } describe('Pi managed resource loader', () => { it('materializes only managed prompt and explicitly paired package Skills', async () => { const fixture = await fixtureRoot(); const prompt = 'PRIVATE PARTNER PROMPT CONTENT'; const effectiveSnapshot: EffectivePluginSnapshot = { accountSessionId: 'account-a\u00001', projectId: 'project-1', pluginReleaseIds: ['release-a'], effectiveSkillIds: ['grilling'], skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir, }], toolDefinitions: [], runtimePolicies: [], unavailableReasons: [], }; const resources = await materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: 'project-1', agentId: 'agent-1', prompt, skillEntries: [ { id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir }, { id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir }, ], catalogRevision: 11, bundledSkillsDir: fixture.skillsDir, effectiveSnapshot, revision: { provider: 3, resources: 7 }, }); expect(await readFile(resources.promptPath, 'utf8')).toBe(prompt); expect(await readFile(resources.languagePromptPath, 'utf8')).toBe(MAKELORE_DEFAULT_LANGUAGE_PROMPT); expect(MAKELORE_DEFAULT_LANGUAGE_PROMPT).toContain('默认使用简体中文'); expect(MAKELORE_DEFAULT_LANGUAGE_PROMPT).toContain('可见的思考过程'); expect(resources.skillIds).toEqual(['grilling']); expect(resources.skillEntries).toEqual([ { id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir }, ]); expect(resources.catalogRevision).toBe(11); expect(resources.skillPaths).toEqual([path.join(fixture.skillsDir, 'grilling', 'SKILL.md')]); expect(JSON.stringify(resources.summary)).not.toContain(prompt); const manifest = JSON.parse(await readFile(resources.manifestPath, 'utf8')) as Record; expect(manifest).toMatchObject({ schemaVersion: 1, projectId: 'project-1', agentId: 'agent-1', promptFile: 'agent-1.md', languagePromptFile: 'agent-1.language.md', skillIds: ['grilling'], skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md', packageRoot: fixture.skillsDir, }], catalogRevision: 11, effectivePluginSnapshot: effectiveSnapshot, revision: { provider: 3, resources: 7 }, }); expect(resources.effectivePluginSnapshot).toEqual(effectiveSnapshot); expect(resources.summary.effectivePluginSnapshot).toEqual(effectiveSnapshot); expect(JSON.stringify(manifest)).not.toContain(prompt); await expect(readFile(path.join(fixture.userDataDir, '.pi', 'agents', 'agent-1.md'), 'utf8')) .rejects.toMatchObject({ code: 'ENOENT' }); }); it('builds argv from managed paths without prompt content, credentials, or auto-discovery roots', async () => { const fixture = await fixtureRoot(); const prompt = 'PROMPT-MUST-NOT-BE-IN-ARGV'; const resources = await materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: 'project-1', agentId: 'agent-1', prompt, skillEntries: [{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md', packageRoot: fixture.skillsDir, }], catalogRevision: 12, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 1 }, }); const selection: PiProviderSelection = { accountId: 'private-account-id', runtimeProviderId: 'makelore-account-opaque', modelId: 'model-a', thinkingLevel: 'medium', input: ['text'], }; const args = buildPiManagedInputArgs(selection, resources); const serialized = JSON.stringify(args); expect(args).toEqual([ '--provider', 'makelore-account-opaque', '--model', 'model-a', '--thinking', 'medium', '--system-prompt', resources.promptPath, '--append-system-prompt', resources.languagePromptPath, '--skill', path.join(fixture.skillsDir, 'agent-browser', 'SKILL.md'), ]); expect(serialized).not.toContain(prompt); expect(serialized).not.toContain(MAKELORE_DEFAULT_LANGUAGE_PROMPT); expect(serialized).not.toContain('private-account-id'); expect(serialized).not.toContain(path.join(fixture.projectDir, '.pi')); expect(serialized).not.toContain(path.join(fixture.projectDir, '.agents')); }); it('materializes effective plugin Skill entries from their bundled package roots', async () => { const fixture = await fixtureRoot(); const resources = await materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: 'project-1', agentId: 'agent-1', prompt: 'plugin prompt', skillEntries: [ { id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }, { id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }, ], catalogRevision: 13, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 1 }, }); expect(resources.skillIds).toEqual(['data-service', 'game-resource']); expect(resources.skillPaths).toEqual([ path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'), path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'), ]); expect(resources.catalogRevision).toBe(13); }); it('never resolves a Skill from the retired standalone resource root', async () => { const fixture = await fixtureRoot(); await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, [ { id: 'grilling', entryPath: 'grilling/SKILL.md' }, ])).rejects.toThrow('Plugin Skill entry is not a file'); }); it('keeps each effective Skill paired with its verified package root when relative paths collide', async () => { const fixture = await fixtureRoot(); const firstRoot = path.join(fixture.root, 'packages', 'first'); const secondRoot = path.join(fixture.root, 'packages', 'second'); const relativeEntry = 'skills/shared/SKILL.md'; await Promise.all([ mkdir(path.join(firstRoot, 'skills', 'shared'), { recursive: true }), mkdir(path.join(secondRoot, 'skills', 'shared'), { recursive: true }), ]); await Promise.all([ writeFile(path.join(firstRoot, relativeEntry), 'first package', 'utf8'), writeFile(path.join(secondRoot, relativeEntry), 'second package', 'utf8'), ]); const resolved = await resolveExplicitCodingSkillPaths(fixture.skillsDir, [ { id: 'first-skill', entryPath: relativeEntry, packageRoot: firstRoot }, { id: 'second-skill', entryPath: relativeEntry, packageRoot: secondRoot }, ]); expect(resolved.skillPaths).toEqual([ path.join(firstRoot, relativeEntry), path.join(secondRoot, relativeEntry), ]); await expect(readFile(resolved.skillPaths[0]!, 'utf8')).resolves.toBe('first package'); await expect(readFile(resolved.skillPaths[1]!, 'utf8')).resolves.toBe('second package'); }); it('ignores a legacy standalone assignment and restores a disabled plugin Skill after re-enable', async () => { const fixture = await fixtureRoot(); const assignedSkillIds = ['grilling', 'data-service']; let enabled = true; const policy: PluginPolicyClientState = { status: 'current', revision: 17, lastVerifiedAt: 1, catalog: { schema_version: 1, catalog_version: 'catalog-17', pricing_version: null, plugins: [{ plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id, supported_contract_versions: [DATA_SERVICE_PLUGIN_DEFINITION.contractVersion], status: 'active', capabilities: DATA_SERVICE_PLUGIN_DEFINITION.skills[0].grants.map((capabilityId) => ({ capability_id: capabilityId, operations: DATA_SERVICE_PLUGIN_DEFINITION.tools .filter((tool) => tool.capabilityId === capabilityId) .map((tool) => ({ operation: tool.operation, billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' }, })), })), }], }, }; const registry = new CodingCapabilityRegistryImpl({ policyClient: { getState: () => policy, refresh: vi.fn().mockResolvedValue(undefined) }, getEnabledPluginIds: async () => enabled ? [DATA_SERVICE_PLUGIN_DEFINITION.id] : [], definitions: [DATA_SERVICE_PLUGIN_DEFINITION], adapters: [{ pluginId: DATA_SERVICE_PLUGIN_DEFINITION.id, async inspect() { return { status: 'ready' }; }, async invoke() { return { success: true, status: 200, code: null, error: null, retryable: false, payload_schema: 'data-service.v1', data: null, }; }, }], }); const resolve = async () => await registry.resolveWorkerResources({ projectPath: fixture.projectDir, assignedSkillIds, role: 'parent', }); const first = await resolve(); expect(first.effectiveSkillIds).toEqual(['data-service']); expect(first.tools).toHaveLength(10); enabled = false; const disabled = await resolve(); expect(assignedSkillIds).toEqual(['grilling', 'data-service']); expect(disabled.effectiveSkillIds).toEqual([]); expect(disabled.tools).toEqual([]); await expect(materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: 'project-1', agentId: 'agent-1', prompt: 'disabled plugin prompt', skillEntries: disabled.skillEntries, catalogRevision: disabled.catalogRevision, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 2 }, })).resolves.toMatchObject({ skillIds: [], catalogRevision: 17 }); enabled = true; const restored = await resolve(); expect(restored.effectiveSkillIds).toEqual(['data-service']); expect(restored.tools).toHaveLength(10); }); it('keeps the platform Chinese-language prompt when the Agent prompt is empty', async () => { const fixture = await fixtureRoot(); const resources = await materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: 'project-1', agentId: 'agent-1', prompt: '', skillEntries: [], catalogRevision: 0, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 1 }, }); expect(await readFile(resources.promptPath, 'utf8')).toBe(''); expect(await readFile(resources.languagePromptPath, 'utf8')).toBe(MAKELORE_DEFAULT_LANGUAGE_PROMPT); }); it('rejects unknown skills and unsafe managed path segments', async () => { const fixture = await fixtureRoot(); await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, [ { id: 'not-bundled', entryPath: 'not-bundled/SKILL.md' }, ])).rejects.toThrow('Plugin Skill entry is not a file'); await expect(materializePiAgentResources({ userDataDir: fixture.userDataDir, projectId: '../outside', agentId: 'agent-1', prompt: '', skillEntries: [], catalogRevision: 1, bundledSkillsDir: fixture.skillsDir, revision: { provider: 1, resources: 1 }, })).rejects.toThrow('Project id'); }); it('uses the same vendor-neutral resource source in development and packaged apps', () => { expect(resolveBundledCodingSkillsDir({ isPackaged: false, resourcesPath: 'C:\\Program Files\\Makelore\\resources', appPath: 'D:\\source\\makelore', })).toBe(path.join('D:\\source\\makelore', 'resources', 'coding-skills')); expect(resolveBundledCodingSkillsDir({ isPackaged: true, resourcesPath: 'C:\\Program Files\\Makelore\\resources', appPath: 'unused', })).toBe(path.join('C:\\Program Files\\Makelore\\resources', 'resources', 'coding-skills')); expect(getPiManagedPaths('D:\\user-data').rootDir) .toBe(path.join(path.resolve('D:\\user-data'), 'coding-runtime', 'pi')); }); it('packages coding skills through the vendor-neutral resources bundle only', async () => { const builder = YAML.parse(await readFile('electron-builder.yml', 'utf8')) as { extraResources: Array<{ from: string; to: string }>; }; expect(builder.extraResources).toContainEqual(expect.objectContaining({ from: 'resources/', to: 'resources/', })); expect(builder.extraResources).not.toEqual(expect.arrayContaining([ expect.objectContaining({ from: '.opencode/skills/' }), expect.objectContaining({ to: 'course-skills/' }), ])); }); });