feat: add Pi provider managed resources
This commit is contained in:
@@ -37,7 +37,7 @@ afterEach(() => {
|
||||
describe('Agent Browser OpenCode plugin', () => {
|
||||
it('loads the bundled runtime plugin artifact', async () => {
|
||||
const bundled = await import(
|
||||
'../../.opencode/skills/agent-browser/.opencode/plugins/niancode-agent-browser.js'
|
||||
'../../resources/coding-extensions/opencode/niancode-agent-browser.js'
|
||||
);
|
||||
|
||||
expect(bundled.NianCodeAgentBrowserPlugin).toBeTypeOf('function');
|
||||
|
||||
@@ -63,10 +63,9 @@ describe('OpencodeManager', () => {
|
||||
appPath: 'C:\\Program Files\\Makelore\\resources\\app.asar',
|
||||
})).toBe(join(
|
||||
resourcesPath,
|
||||
'course-skills',
|
||||
'agent-browser',
|
||||
'.opencode',
|
||||
'plugins',
|
||||
'resources',
|
||||
'coding-extensions',
|
||||
'opencode',
|
||||
'niancode-agent-browser.js',
|
||||
));
|
||||
});
|
||||
|
||||
225
tests/unit/pi-provider-config.test.ts
Normal file
225
tests/unit/pi-provider-config.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { 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 type { ProviderAccount } from '@electron/shared/providers/types';
|
||||
import {
|
||||
buildPiProviderCatalog,
|
||||
buildPiWorkerCredentialProjection,
|
||||
credentialValueForProviderSecret,
|
||||
PiProviderConfigError,
|
||||
resolvePiRuntimeProviderId,
|
||||
selectPiProviderModel,
|
||||
summarizePiWorkerCredentialProjection,
|
||||
writePiProviderCatalog,
|
||||
} from '@electron/coding-runtime/pi/provider-config';
|
||||
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
function account(overrides: Partial<ProviderAccount> = {}): ProviderAccount {
|
||||
return {
|
||||
id: 'account-one',
|
||||
vendorId: 'openai',
|
||||
label: 'Primary account',
|
||||
authMode: 'api_key',
|
||||
model: 'gpt-5.4',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
createdAt: '2026-08-22T00:00:00.000Z',
|
||||
updatedAt: '2026-08-22T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Pi Provider catalog', () => {
|
||||
it('maps every current Provider protocol shape to a Pi API', () => {
|
||||
const catalog = buildPiProviderCatalog({
|
||||
accounts: [
|
||||
account({ id: 'completions', vendorId: 'custom', apiProtocol: 'openai-completions', baseUrl: 'https://one.test/v1', model: 'chat' }),
|
||||
account({ id: 'responses', vendorId: 'custom', apiProtocol: 'openai-responses', baseUrl: 'https://two.test/v1', model: 'response' }),
|
||||
account({ id: 'anthropic', vendorId: 'anthropic', model: 'claude-opus-4-6' }),
|
||||
account({ id: 'google', vendorId: 'google', model: 'gemini-3-pro-preview' }),
|
||||
account({ id: 'openrouter', vendorId: 'openrouter', model: 'openai/gpt-5.4' }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(catalog.descriptors.map(({ api }) => api)).toEqual([
|
||||
'openai-completions',
|
||||
'openai-responses',
|
||||
'anthropic-messages',
|
||||
'google-generative-ai',
|
||||
'openai-completions',
|
||||
]);
|
||||
expect(catalog.descriptors.at(-1)?.models[0]?.compat).toEqual({
|
||||
thinkingFormat: 'openrouter',
|
||||
sessionAffinityFormat: 'openrouter',
|
||||
});
|
||||
expect(Object.keys(catalog.descriptors.at(-1)?.headers ?? {}).sort()).toEqual([
|
||||
'HTTP-Referer',
|
||||
'X-OpenRouter-Title',
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses stable collision-free account-derived runtime IDs for the same vendor', () => {
|
||||
const first = account({ id: 'openai-account-a' });
|
||||
const second = account({ id: 'openai-account-b', isDefault: false });
|
||||
const catalog = buildPiProviderCatalog({ accounts: [first, second] });
|
||||
|
||||
expect(catalog.descriptors[0]?.runtimeProviderId).toBe(resolvePiRuntimeProviderId(first.id));
|
||||
expect(catalog.descriptors[1]?.runtimeProviderId).toBe(resolvePiRuntimeProviderId(second.id));
|
||||
expect(catalog.descriptors[0]?.runtimeProviderId).not.toBe(catalog.descriptors[1]?.runtimeProviderId);
|
||||
});
|
||||
|
||||
it('normalizes Works gateway /v1 and keeps all credential and header values out of catalog output', async () => {
|
||||
const provider = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
apiProtocol: 'openai-completions',
|
||||
baseUrl: 'https://gateway.test/',
|
||||
model: 'qwen3.6-plus',
|
||||
headers: {
|
||||
'X-Works-Square-AI-Token': '{env:OLD_GATEWAY_TOKEN}',
|
||||
'X-Tenant-Secret': 'private-tenant-header',
|
||||
},
|
||||
metadata: {
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway',
|
||||
customModels: ['qwen3.6-plus'],
|
||||
},
|
||||
});
|
||||
const catalog = buildPiProviderCatalog({ accounts: [provider] });
|
||||
const descriptor = catalog.descriptors[0]!;
|
||||
const serialized = JSON.stringify(catalog);
|
||||
|
||||
expect(descriptor.baseUrl).toBe('https://gateway.test/v1');
|
||||
expect(Object.keys(descriptor.headers).sort()).toEqual([
|
||||
'Authorization',
|
||||
'X-Tenant-Secret',
|
||||
'X-Works-Square-AI-Token',
|
||||
]);
|
||||
expect(descriptor.models[0]).toMatchObject({
|
||||
id: 'qwen3.6-plus',
|
||||
input: ['text', 'image'],
|
||||
contextWindow: 1_000_000,
|
||||
maxOutputTokens: 65_536,
|
||||
});
|
||||
expect(serialized).not.toContain('private-tenant-header');
|
||||
expect(serialized).not.toContain('OLD_GATEWAY_TOKEN');
|
||||
|
||||
const projection = await buildPiWorkerCredentialProjection({
|
||||
account: provider,
|
||||
descriptor,
|
||||
resolveCredential: vi.fn().mockResolvedValue('gateway-proxy-token'),
|
||||
});
|
||||
expect(Object.values(projection.env)).toEqual(expect.arrayContaining([
|
||||
'gateway-proxy-token',
|
||||
'Bearer gateway-proxy-token',
|
||||
'private-tenant-header',
|
||||
]));
|
||||
expect(projection.sensitiveValues).toContain('gateway-proxy-token');
|
||||
const safeProjection = summarizePiWorkerCredentialProjection(projection);
|
||||
expect(JSON.stringify(safeProjection)).not.toContain('gateway-proxy-token');
|
||||
expect(JSON.stringify(safeProjection)).not.toContain('private-tenant-header');
|
||||
});
|
||||
|
||||
it('uses only the current worker local-proxy credential for proxy mode', async () => {
|
||||
const provider = account({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
apiProtocol: 'openai-completions',
|
||||
baseUrl: 'http://127.0.0.1:54321/api/ai-proxy/v1',
|
||||
model: 'qwen-vl-max',
|
||||
metadata: { worksSquareCredentialMode: 'works_square_ai_gateway_proxy' },
|
||||
});
|
||||
const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!;
|
||||
const resolveCredential = vi.fn().mockResolvedValue('stale-stored-host-token');
|
||||
const projection = await buildPiWorkerCredentialProjection({
|
||||
account: provider,
|
||||
descriptor,
|
||||
resolveCredential,
|
||||
localProxyCredential: 'current-worker-host-token',
|
||||
});
|
||||
|
||||
expect(resolveCredential).not.toHaveBeenCalled();
|
||||
expect(Object.values(projection.env)).toContain('current-worker-host-token');
|
||||
expect(Object.values(projection.env)).not.toContain('stale-stored-host-token');
|
||||
});
|
||||
|
||||
it('uses account-scoped model capability metadata and rejects unavailable models', () => {
|
||||
const provider = account({ id: 'account-with-vision', model: 'vision-model' });
|
||||
const catalog = buildPiProviderCatalog({
|
||||
accounts: [provider],
|
||||
modelSummaries: [{
|
||||
id: 'vision-model',
|
||||
name: 'Vision model',
|
||||
vendorId: 'openai',
|
||||
accountId: provider.id,
|
||||
supportsVision: true,
|
||||
supportsReasoning: true,
|
||||
contextWindow: 200_000,
|
||||
source: 'remote',
|
||||
}],
|
||||
});
|
||||
expect(selectPiProviderModel(catalog, {
|
||||
accountId: provider.id,
|
||||
modelId: 'vision-model',
|
||||
thinkingLevel: 'high',
|
||||
})).toMatchObject({
|
||||
input: ['text', 'image'],
|
||||
contextWindow: 200_000,
|
||||
thinkingLevel: 'high',
|
||||
});
|
||||
expect(() => selectPiProviderModel(catalog, {
|
||||
accountId: provider.id,
|
||||
modelId: 'missing-model',
|
||||
thinkingLevel: 'off',
|
||||
})).toThrowError(PiProviderConfigError);
|
||||
});
|
||||
|
||||
it('does not replace an existing catalog when model selection is unavailable', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-provider-'));
|
||||
temporaryRoots.push(root);
|
||||
const filePath = path.join(root, 'models.json');
|
||||
await writeFile(filePath, 'existing-catalog\n', 'utf8');
|
||||
const catalog = buildPiProviderCatalog({ accounts: [account()] });
|
||||
|
||||
await expect(writePiProviderCatalog(filePath, catalog, {
|
||||
accountId: 'account-one',
|
||||
modelId: 'not-configured',
|
||||
thinkingLevel: 'off',
|
||||
})).rejects.toMatchObject({ code: 'MODEL_UNAVAILABLE' });
|
||||
expect(await readFile(filePath, 'utf8')).toBe('existing-catalog\n');
|
||||
});
|
||||
|
||||
it('fails closed when a non-local credential is unavailable', async () => {
|
||||
const provider = account();
|
||||
const descriptor = buildPiProviderCatalog({ accounts: [provider] }).descriptors[0]!;
|
||||
await expect(buildPiWorkerCredentialProjection({
|
||||
account: provider,
|
||||
descriptor,
|
||||
resolveCredential: vi.fn().mockResolvedValue(null),
|
||||
})).rejects.toMatchObject({ code: 'PROVIDER_AUTH_REQUIRED' });
|
||||
});
|
||||
|
||||
it('projects API key, OAuth, and local secrets to one worker credential value', () => {
|
||||
expect(credentialValueForProviderSecret({
|
||||
type: 'api_key',
|
||||
accountId: 'account-one',
|
||||
apiKey: 'api-key-value',
|
||||
})).toBe('api-key-value');
|
||||
expect(credentialValueForProviderSecret({
|
||||
type: 'oauth',
|
||||
accountId: 'account-one',
|
||||
accessToken: 'oauth-access-token',
|
||||
refreshToken: 'refresh-token-never-projected',
|
||||
expiresAt: Date.now() + 60_000,
|
||||
})).toBe('oauth-access-token');
|
||||
expect(credentialValueForProviderSecret({
|
||||
type: 'local',
|
||||
accountId: 'account-one',
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
95
tests/unit/pi-provider-state.test.ts
Normal file
95
tests/unit/pi-provider-state.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { PiManagedInputRevisionCoordinator } from '@electron/coding-runtime/pi/managed-input-revision';
|
||||
import { PiProviderRefreshCoordinator } from '@electron/coding-runtime/pi/provider-refresh';
|
||||
|
||||
describe('Pi Provider refresh coordination', () => {
|
||||
it('coalesces concurrent refreshes for one account but not different accounts', async () => {
|
||||
const coordinator = new PiProviderRefreshCoordinator();
|
||||
let releaseFirst!: () => void;
|
||||
const firstWait = new Promise<void>((resolve) => { releaseFirst = resolve; });
|
||||
const sameAccountRefresh = vi.fn(async () => { await firstWait; });
|
||||
const otherAccountRefresh = vi.fn(async () => undefined);
|
||||
|
||||
const first = coordinator.refreshAccount('account-a', sameAccountRefresh);
|
||||
const second = coordinator.refreshAccount('account-a', sameAccountRefresh);
|
||||
const other = coordinator.refreshAccount('account-b', otherAccountRefresh);
|
||||
await other;
|
||||
expect(coordinator.pendingAccountCount).toBe(1);
|
||||
releaseFirst();
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(sameAccountRefresh).toHaveBeenCalledTimes(1);
|
||||
expect(otherAccountRefresh).toHaveBeenCalledTimes(1);
|
||||
expect(coordinator.pendingAccountCount).toBe(0);
|
||||
});
|
||||
|
||||
it('refreshes and reopens at most once for one failed operation', async () => {
|
||||
const coordinator = new PiProviderRefreshCoordinator();
|
||||
const authError = new Error('401');
|
||||
const operation = vi.fn(async () => { throw authError; });
|
||||
const refreshCredential = vi.fn(async () => undefined);
|
||||
const reopenWorker = vi.fn(async () => undefined);
|
||||
|
||||
await expect(coordinator.withSingleAuthRecovery({
|
||||
accountId: 'account-a',
|
||||
operation,
|
||||
isAuthenticationError: (error) => error === authError,
|
||||
refreshCredential,
|
||||
reopenWorker,
|
||||
})).rejects.toBe(authError);
|
||||
|
||||
expect(operation).toHaveBeenCalledTimes(2);
|
||||
expect(operation.mock.calls.map(([attempt]) => attempt)).toEqual([0, 1]);
|
||||
expect(refreshCredential).toHaveBeenCalledTimes(1);
|
||||
expect(reopenWorker).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not refresh for a non-authentication failure', async () => {
|
||||
const coordinator = new PiProviderRefreshCoordinator();
|
||||
const failure = new Error('timeout');
|
||||
const refreshCredential = vi.fn(async () => undefined);
|
||||
const reopenWorker = vi.fn(async () => undefined);
|
||||
await expect(coordinator.withSingleAuthRecovery({
|
||||
accountId: 'account-a',
|
||||
operation: async () => { throw failure; },
|
||||
isAuthenticationError: () => false,
|
||||
refreshCredential,
|
||||
reopenWorker,
|
||||
})).rejects.toBe(failure);
|
||||
expect(refreshCredential).not.toHaveBeenCalled();
|
||||
expect(reopenWorker).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pi managed input revision coordination', () => {
|
||||
it('rebuilds an idle stale worker before its next prompt', () => {
|
||||
const coordinator = new PiManagedInputRevisionCoordinator();
|
||||
coordinator.registerWorker('worker');
|
||||
coordinator.markProviderStale();
|
||||
|
||||
expect(coordinator.beforePrompt('worker')).toEqual({
|
||||
action: 'rebuild-before-prompt',
|
||||
revision: { provider: 2, resources: 1 },
|
||||
});
|
||||
coordinator.applyCurrentRevision('worker');
|
||||
expect(coordinator.beforePrompt('worker').action).toBe('reuse');
|
||||
});
|
||||
|
||||
it('keeps a running snapshot and schedules rebuild only after settlement', () => {
|
||||
const coordinator = new PiManagedInputRevisionCoordinator();
|
||||
coordinator.registerWorker('worker');
|
||||
const runRevision = coordinator.beginRun('worker');
|
||||
coordinator.markResourcesStale();
|
||||
|
||||
expect(runRevision).toEqual({ provider: 1, resources: 1 });
|
||||
expect(coordinator.beforePrompt('worker')).toEqual({
|
||||
action: 'defer-until-settled',
|
||||
revision: { provider: 1, resources: 1 },
|
||||
});
|
||||
expect(() => coordinator.applyCurrentRevision('worker')).toThrow('run is active');
|
||||
expect(coordinator.settleRun('worker')).toEqual({
|
||||
action: 'rebuild-after-settled',
|
||||
revision: { provider: 1, resources: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
157
tests/unit/pi-resource-loader.test.ts
Normal file
157
tests/unit/pi-resource-loader.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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 } from 'vitest';
|
||||
import YAML from 'yaml';
|
||||
import type { PiProviderSelection } from '@electron/coding-runtime/pi/provider-config';
|
||||
import {
|
||||
buildPiManagedInputArgs,
|
||||
getPiManagedPaths,
|
||||
materializePiAgentResources,
|
||||
resolveBundledCodingSkillsDir,
|
||||
resolveExplicitCodingSkillPaths,
|
||||
} from '@electron/coding-runtime/pi/resource-loader';
|
||||
|
||||
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 }),
|
||||
]);
|
||||
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'),
|
||||
]);
|
||||
return { root, userDataDir, projectDir, skillsDir };
|
||||
}
|
||||
|
||||
describe('Pi managed resource loader', () => {
|
||||
it('materializes only managed prompt and explicitly selected bundled skills', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
const prompt = 'PRIVATE PARTNER PROMPT CONTENT';
|
||||
const resources = await materializePiAgentResources({
|
||||
userDataDir: fixture.userDataDir,
|
||||
projectId: 'project-1',
|
||||
agentId: 'agent-1',
|
||||
prompt,
|
||||
skillIds: ['grilling', 'grilling'],
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
revision: { provider: 3, resources: 7 },
|
||||
});
|
||||
|
||||
expect(await readFile(resources.promptPath, 'utf8')).toBe(prompt);
|
||||
expect(resources.skillIds).toEqual(['grilling']);
|
||||
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<string, unknown>;
|
||||
expect(manifest).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
projectId: 'project-1',
|
||||
agentId: 'agent-1',
|
||||
promptFile: 'agent-1.md',
|
||||
skillIds: ['grilling'],
|
||||
revision: { provider: 3, resources: 7 },
|
||||
});
|
||||
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,
|
||||
skillIds: ['agent-browser'],
|
||||
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,
|
||||
'--skill', path.join(fixture.skillsDir, 'agent-browser', 'SKILL.md'),
|
||||
]);
|
||||
expect(serialized).not.toContain(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('rejects unknown skills and unsafe managed path segments', async () => {
|
||||
const fixture = await fixtureRoot();
|
||||
await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, ['not-bundled']))
|
||||
.rejects.toThrow('Unknown bundled coding skill');
|
||||
await expect(materializePiAgentResources({
|
||||
userDataDir: fixture.userDataDir,
|
||||
projectId: '../outside',
|
||||
agentId: 'agent-1',
|
||||
prompt: '',
|
||||
skillIds: [],
|
||||
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/' }),
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PiWorkerProcess,
|
||||
buildPiRpcArgs,
|
||||
sanitizePiDiagnostic,
|
||||
buildPiWorkerEnvironment,
|
||||
} from '../../electron/coding-runtime/pi/worker-process';
|
||||
|
||||
const fakeChildPath = resolve('tests/fixtures/fake-pi-rpc-child.mjs');
|
||||
@@ -254,6 +255,39 @@ describe('Pi worker process', () => {
|
||||
expect(worker.stderrDiagnostic).toContain('[REDACTED]');
|
||||
expect(Buffer.byteLength(worker.stderrDiagnostic)).toBeLessThanOrEqual(160);
|
||||
expect(sanitizePiDiagnostic(`token=${secret}`, [secret])).toBe('token=[REDACTED]');
|
||||
expect(sanitizePiDiagnostic('custom-header=q', ['q'])).toBe('custom-header=[REDACTED]');
|
||||
});
|
||||
|
||||
it('inherits only the worker-safe environment allowlist', () => {
|
||||
const env = buildPiWorkerEnvironment(
|
||||
'D:\\managed-pi',
|
||||
{ MAKELore_PI_SELECTED_API_KEY: 'selected-secret' },
|
||||
{
|
||||
PATH: 'D:\\tools',
|
||||
OPENAI_API_KEY: 'unrelated-openai-secret',
|
||||
ANTHROPIC_API_KEY: 'unrelated-anthropic-secret',
|
||||
CUSTOM_APPLICATION_SECRET: 'unrelated-custom-secret',
|
||||
},
|
||||
);
|
||||
expect(env).toMatchObject({
|
||||
PATH: 'D:\\tools',
|
||||
MAKELore_PI_SELECTED_API_KEY: 'selected-secret',
|
||||
PI_CODING_AGENT_DIR: 'D:\\managed-pi',
|
||||
PI_OFFLINE: '1',
|
||||
PI_TELEMETRY: '0',
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
});
|
||||
expect(env).not.toHaveProperty('OPENAI_API_KEY');
|
||||
expect(env).not.toHaveProperty('ANTHROPIC_API_KEY');
|
||||
expect(env).not.toHaveProperty('CUSTOM_APPLICATION_SECRET');
|
||||
});
|
||||
|
||||
it('refuses to put a selected worker credential in argv', async () => {
|
||||
const secret = 'argv-secret-value';
|
||||
await expect(makeWorker({
|
||||
additionalArgs: ['--api-key', secret],
|
||||
sensitiveValues: [secret],
|
||||
})).rejects.toThrow('arguments contain a sensitive value');
|
||||
});
|
||||
|
||||
it('forces the complete child tree down after the graceful deadline', async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest';
|
||||
describe('planning-with-files Skill', () => {
|
||||
it('requires planning files directly in the current project root', async () => {
|
||||
const skill = await readFile(
|
||||
path.join(process.cwd(), '.opencode', 'skills', 'planning-with-files', 'SKILL.md'),
|
||||
path.join(process.cwd(), 'resources', 'coding-skills', 'planning-with-files', 'SKILL.md'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user