Skip to content
Closed
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
40 changes: 39 additions & 1 deletion src/__tests__/autoBootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type McpServerConfig = {
};

type AgentConfig = {
mode?: string;
tools?: Record<string, unknown>;
};

Expand Down Expand Up @@ -78,10 +79,12 @@ describe('autoBootstrapPalantirMcpIfConfigured', () => {

expect(cfg.agent?.['foundry-librarian']).toBeTruthy();
expect(cfg.agent?.foundry).toBeTruthy();
expect(cfg.agent?.['foundry-librarian']?.mode).toBe('subagent');
expect(cfg.agent?.foundry?.mode).toBe('all');

expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_list_datasets']).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_get_dataset']).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_create_thing']).toBe(false);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_create_thing']).toBe(true);
});

it('is idempotent for repeated runs', async () => {
Expand Down Expand Up @@ -130,4 +133,39 @@ describe('autoBootstrapPalantirMcpIfConfigured', () => {
await autoBootstrapPalantirMcpIfConfigured(tmpDir);
expect(spy).not.toHaveBeenCalled();
});

it('preserves explicit foundry mode during bootstrap patching', async () => {
process.env.FOUNDRY_TOKEN = 'TEST_TOKEN';
process.env.FOUNDRY_URL = 'https://example.palantirfoundry.com';

vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets']);

const cfgPath: string = path.join(tmpDir, 'opencode.jsonc');
const existing: OpencodeConfig = {
mcp: {
'palantir-mcp': {
type: 'local',
command: [
'npx',
'-y',
'palantir-mcp',
'--foundry-api-url',
'https://example.palantirfoundry.com',
],
environment: { FOUNDRY_TOKEN: '{env:FOUNDRY_TOKEN}' },
},
},
agent: {
foundry: {
mode: 'subagent',
},
},
};
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));

await autoBootstrapPalantirMcpIfConfigured(tmpDir);

const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) as OpencodeConfig;
expect(cfg.agent?.foundry?.mode).toBe('subagent');
});
});
9 changes: 6 additions & 3 deletions src/__tests__/configHook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ describe('plugin config hook', () => {
let tmpDir: string;
let priorToken: string | undefined;
let priorUrl: string | undefined;
let ensureDocsSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-config-test-'));
Expand All @@ -22,7 +21,7 @@ describe('plugin config hook', () => {
delete process.env.FOUNDRY_TOKEN;
delete process.env.FOUNDRY_URL;

ensureDocsSpy = vi.spyOn(snapshotModule, 'ensureDocsParquet').mockResolvedValue({
vi.spyOn(snapshotModule, 'ensureDocsParquet').mockResolvedValue({
dbPath: path.join(tmpDir, 'data', 'docs.parquet'),
changed: false,
source: 'existing',
Expand Down Expand Up @@ -53,7 +52,9 @@ describe('plugin config hook', () => {

expect(cfg.agent?.['foundry-librarian']).toBeTruthy();
expect(cfg.agent?.foundry).toBeTruthy();
expect(ensureDocsSpy).toHaveBeenCalledWith(
expect(cfg.agent?.['foundry-librarian']?.mode).toBe('subagent');
expect(cfg.agent?.foundry?.mode).toBe('all');
expect(snapshotModule.ensureDocsParquet).toHaveBeenCalledWith(
expect.objectContaining({
dbPath: path.join(tmpDir, 'data', 'docs.parquet'),
force: false,
Expand All @@ -74,6 +75,7 @@ describe('plugin config hook', () => {
},
agent: {
foundry: {
mode: 'subagent',
prompt: 'CUSTOM_PROMPT',
tools: {
get_doc_page: true,
Expand All @@ -87,6 +89,7 @@ describe('plugin config hook', () => {
expect(cfg.command?.['refresh-docs']?.template).toBe('CUSTOM_TEMPLATE');
expect(cfg.command?.['refresh-docs']?.description).toBe('CUSTOM_DESCRIPTION');

expect(cfg.agent?.foundry?.mode).toBe('subagent');
expect(cfg.agent?.foundry?.prompt).toBe('CUSTOM_PROMPT');
expect(cfg.agent?.foundry?.tools?.get_doc_page).toBe(true);
// Additive defaults are allowed.
Expand Down
84 changes: 81 additions & 3 deletions src/__tests__/palantirMcpRescan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type CommandHookOutput = { parts: unknown[] };
type CommandHook = (input: CommandHookInput, output: CommandHookOutput) => Promise<void>;

type OpencodeConfig = {
agent?: Record<string, { description?: string; tools?: Record<string, unknown> }>;
agent?: Record<string, { description?: string; mode?: string; tools?: Record<string, unknown> }>;
};

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down Expand Up @@ -47,14 +47,14 @@ describe('/rescan-palantir-mcp-tools', () => {
else process.env.FOUNDRY_TOKEN = priorToken;
});

async function runRescan(): Promise<{ text: string }> {
async function runRescan(args = ''): Promise<{ text: string }> {
const hooks = await plugin({ worktree: tmpDir });
const hook = hooks['command.execute.before'];
if (typeof hook !== 'function') throw new Error('Missing command.execute.before hook');

const output: CommandHookOutput = { parts: [] };
await (hook as CommandHook)(
{ command: 'rescan-palantir-mcp-tools', sessionID: 'test-session', arguments: '' },
{ command: 'rescan-palantir-mcp-tools', sessionID: 'test-session', arguments: args },
output
);
return { text: getFirstTextPart(output) };
Expand All @@ -68,6 +68,12 @@ describe('/rescan-palantir-mcp-tools', () => {
expect(spy).not.toHaveBeenCalled();
});

it('returns a validation error for invalid profile override', async () => {
const result = await runRescan('--profile nope');
expect(result.text).toContain('invalid');
expect(result.text).toContain('Valid values');
});

it('preserves existing palantir-mcp_* toggles and adds missing ones', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets', 'get_dataset']);

Expand Down Expand Up @@ -107,11 +113,83 @@ describe('/rescan-palantir-mcp-tools', () => {
'Generated by opencode-palantir /setup-palantir-mcp.'
);
expect(cfg.agent?.foundry?.description).toContain('Profile:');
expect(cfg.agent?.foundry?.mode).toBe('all');

expect(cfg.agent?.foundry?.tools?.['palantir-mcp_list_datasets']).toBe(false);
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_get_dataset']).toBe(true);
});

it('preserves explicit foundry mode during rescan', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets']);

const cfgPath = path.join(tmpDir, 'opencode.jsonc');
const seeded = {
mcp: {
'palantir-mcp': {
type: 'local',
command: [
'npx',
'-y',
'palantir-mcp',
'--foundry-api-url',
'https://example.palantirfoundry.com',
],
environment: { FOUNDRY_TOKEN: '{env:FOUNDRY_TOKEN}' },
},
},
agent: {
foundry: {
mode: 'subagent',
},
},
};
fs.writeFileSync(cfgPath, JSON.stringify(seeded, null, 2));

await runRescan();

const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) as OpencodeConfig;
expect(cfg.agent?.foundry?.mode).toBe('subagent');
});

it('applies profile override and reports detected profile in output', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue([
'connect_to_dev_console_app',
'delete_foundry_object_type',
]);

fs.mkdirSync(path.join(tmpDir, 'transforms'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'transforms', 't.py'), 'def transform():\n return 1\n');

const cfgPath = path.join(tmpDir, 'opencode.jsonc');
const seeded = {
mcp: {
'palantir-mcp': {
type: 'local',
command: [
'npx',
'-y',
'palantir-mcp',
'--foundry-api-url',
'https://example.palantirfoundry.com',
],
environment: { FOUNDRY_TOKEN: '{env:FOUNDRY_TOKEN}' },
},
},
tools: { 'palantir-mcp_*': false },
agent: {},
};
fs.writeFileSync(cfgPath, JSON.stringify(seeded, null, 2));

const result = await runRescan('--profile compute_modules');
expect(result.text).toContain('Selected profile: compute_modules');
expect(result.text).toContain('Profile source: override (--profile)');
expect(result.text).toContain('Detected profile: pipelines_transforms');

const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) as OpencodeConfig;
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_connect_to_dev_console_app']).toBe(true);
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_delete_foundry_object_type']).toBe(false);
});

it('fails safely on invalid jsonc', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets']);

Expand Down
62 changes: 60 additions & 2 deletions src/__tests__/palantirMcpSetup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type McpServerConfig = {

type AgentConfig = {
description?: string;
mode?: string;
tools?: Record<string, unknown>;
};

Expand Down Expand Up @@ -88,6 +89,14 @@ describe('/setup-palantir-mcp', () => {
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false);
});

it('returns a validation error for an invalid profile override', async () => {
const result = await runSetup('https://example.palantirfoundry.com --profile nope');

expect(result.text).toContain('invalid');
expect(result.text).toContain('Valid values');
expect(fs.existsSync(path.join(tmpDir, 'opencode.jsonc'))).toBe(false);
});

it('normalizes URL and writes mcp server config', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets']);

Expand Down Expand Up @@ -190,18 +199,67 @@ describe('/setup-palantir-mcp', () => {

expect(cfg.agent?.['foundry-librarian']?.tools?.get_doc_page).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.tools?.list_all_docs).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.mode).toBe('subagent');

// execution agent defaults to no docs tools
expect(cfg.agent?.foundry?.tools?.get_doc_page).toBe(false);
expect(cfg.agent?.foundry?.tools?.list_all_docs).toBe(false);
expect(cfg.agent?.foundry?.mode).toBe('all');

expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_list_datasets']).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_get_dataset']).toBe(true);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_create_thing']).toBe(false);
expect(cfg.agent?.['foundry-librarian']?.tools?.['palantir-mcp_create_thing']).toBe(true);

expect(cfg.agent?.foundry?.tools?.['palantir-mcp_list_datasets']).toBe(true);
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_get_dataset']).toBe(true);
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_create_thing']).toBe(false);
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_create_thing']).toBe(true);
});

it('preserves explicit foundry mode when already set', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue(['list_datasets']);

const cfgPath = path.join(tmpDir, 'opencode.jsonc');
const existing = {
agent: {
foundry: {
mode: 'subagent',
},
},
};
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));

await runSetup('https://example.palantirfoundry.com');
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')) as OpencodeConfig;

expect(cfg.agent?.foundry?.mode).toBe('subagent');
});

it('uses profile override when provided and reports detected profile', async () => {
vi.spyOn(mcpClient, 'listPalantirMcpTools').mockResolvedValue([
'connect_to_dev_console_app',
'delete_foundry_object_type',
]);

fs.mkdirSync(path.join(tmpDir, 'transforms'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'transforms', 't.py'), 'def transform():\n return 1\n');

const result = await runSetup(
'https://example.palantirfoundry.com --profile compute_modules_ts'
);

expect(result.text).toContain('Selected profile: compute_modules_ts');
expect(result.text).toContain('Profile source: override (--profile)');
expect(result.text).toContain('Detected profile: pipelines_transforms');

const cfg = JSON.parse(
fs.readFileSync(path.join(tmpDir, 'opencode.jsonc'), 'utf8')
) as OpencodeConfig;
expect(cfg.agent?.foundry?.description).toContain('Profile: compute_modules_ts');

// Broad defaults keep compute click-ops enabled by default.
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_connect_to_dev_console_app']).toBe(true);
// Hard destructive deny list still applies.
expect(cfg.agent?.foundry?.tools?.['palantir-mcp_delete_foundry_object_type']).toBe(false);
});

it('is idempotent for repeated runs', async () => {
Expand Down
35 changes: 35 additions & 0 deletions src/__tests__/profilePolicy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';

import { computeAllowedTools } from '../palantir-mcp/allowlist.ts';

describe('profile policy allowlists', () => {
it('uses broad defaults for compute profiles and only denies hard-destructive tools', () => {
const allowlist = computeAllowedTools('compute_modules', [
'connect_to_dev_console_app',
'create_foundry_branch',
'delete_foundry_object_type',
]);

expect(allowlist.policy.id).toBe('compute_modules');
expect(allowlist.policy.librarianDefaultAllow).toBe('all');
expect(allowlist.policy.foundryDefaultAllow).toBe('all');

expect(allowlist.librarianAllow.has('connect_to_dev_console_app')).toBe(true);
expect(allowlist.foundryAllow.has('connect_to_dev_console_app')).toBe(true);

expect(allowlist.librarianAllow.has('create_foundry_branch')).toBe(true);
expect(allowlist.foundryAllow.has('create_foundry_branch')).toBe(true);

expect(allowlist.librarianAllow.has('delete_foundry_object_type')).toBe(false);
expect(allowlist.foundryAllow.has('delete_foundry_object_type')).toBe(false);
expect(allowlist.policy.deniedTools).toContain('delete_foundry_object_type');
});

it('keeps unknown profile broad by default to optimize usability', () => {
const allowlist = computeAllowedTools('unknown', ['create_foundry_branch']);

expect(allowlist.policy.id).toBe('unknown');
expect(allowlist.librarianAllow.has('create_foundry_branch')).toBe(true);
expect(allowlist.foundryAllow.has('create_foundry_branch')).toBe(true);
});
});
Loading
Loading