Skip to content
Open
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
9 changes: 9 additions & 0 deletions apps/web/src/lib/ai/core/__tests__/ai-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ vi.mock('../../tools/web-search-tools', () => ({
},
}));

vi.mock('../../tools/activity-tools', () => ({
activityTools: {
get_activity: { name: 'get_activity', description: 'Get activity' },
},
}));

import { pageSpaceTools } from '../ai-tools';
import { driveTools } from '../../tools/drive-tools';
import { pageReadTools } from '../../tools/page-read-tools';
Expand All @@ -81,6 +87,7 @@ import { taskManagementTools } from '../../tools/task-management-tools';
import { agentTools } from '../../tools/agent-tools';
import { agentCommunicationTools } from '../../tools/agent-communication-tools';
import { webSearchTools } from '../../tools/web-search-tools';
import { activityTools } from '../../tools/activity-tools';

describe('ai-tools', () => {
describe('pageSpaceTools aggregation', () => {
Expand All @@ -94,6 +101,7 @@ describe('ai-tools', () => {
...agentTools,
...agentCommunicationTools,
...webSearchTools,
...activityTools,
});
});

Expand All @@ -107,6 +115,7 @@ describe('ai-tools', () => {
Object.keys(agentTools),
Object.keys(agentCommunicationTools),
Object.keys(webSearchTools),
Object.keys(activityTools),
];

const allKeys = moduleKeysets.flat();
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/ai/core/ai-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { taskManagementTools } from '../tools/task-management-tools';
import { agentTools } from '../tools/agent-tools';
import { agentCommunicationTools } from '../tools/agent-communication-tools';
import { webSearchTools } from '../tools/web-search-tools';
import { activityTools } from '../tools/activity-tools';

/**
* PageSpace AI Tools - Internal AI SDK tool implementations
Expand All @@ -21,6 +22,7 @@ export const pageSpaceTools = {
...agentTools,
...agentCommunicationTools,
...webSearchTools,
...activityTools,
};

export type PageSpaceTools = typeof pageSpaceTools;
1 change: 1 addition & 0 deletions apps/web/src/lib/ai/core/tool-filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export function getToolsSummary(isReadOnly: boolean, webSearchEnabled = true): {
'list_trash',
'list_agents',
'multi_drive_list_agents',
'get_activity',
// Search tools
'regex_search',
'glob_search',
Expand Down
103 changes: 103 additions & 0 deletions apps/web/src/lib/ai/tools/__tests__/activity-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock boundaries
vi.mock('@pagespace/lib', () => ({
isUserDriveMember: vi.fn(),
}));

import { activityTools } from '../activity-tools';
import { isUserDriveMember } from '@pagespace/lib';
import type { ToolExecutionContext } from '../../core';

const mockIsUserDriveMember = vi.mocked(isUserDriveMember);

// Properly typed test input matching the Zod schema with defaults
type ActivityToolInput = {
since: '1h' | '24h' | '7d' | '30d' | 'last_visit';
excludeOwnActivity: boolean;
includeAiChanges: boolean;
limit: number;
maxOutputChars: number;
includeDiffs: boolean;
driveIds?: string[];
operationCategories?: ('content' | 'permissions' | 'membership')[];
};

// Default values matching the Zod schema defaults
const createTestInput = (overrides: Partial<ActivityToolInput> = {}): ActivityToolInput => ({
since: '24h',
excludeOwnActivity: false,
includeAiChanges: true,
limit: 50,
maxOutputChars: 20000,
includeDiffs: true,
...overrides,
});

/**
* @scaffold - happy path coverage deferred
*
* These tests cover authentication and authorization error paths.
* Happy path tests (actual activity results, grouping, truncation) are deferred
* because they require either:
* - An ActivityRepository seam to avoid complex DB mocking, OR
* - Integration tests against a real database with seeded activity logs
*
* TODO: Add integration tests for:
* - Activity grouping by drive
* - Compact delta generation
* - Progressive truncation under size limits
*/
describe('activity-tools', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('get_activity', () => {
it('has correct tool definition', () => {
expect(activityTools.get_activity).toBeDefined();
expect(activityTools.get_activity.description).toBeDefined();
expect(activityTools.get_activity.description).toContain('activity');
});

it('requires user authentication', async () => {
const context = { toolCallId: '1', messages: [], experimental_context: {} };

await expect(
activityTools.get_activity.execute!(createTestInput(), context)
).rejects.toThrow('User authentication required');
});

it('throws error when specified drive access denied', async () => {
mockIsUserDriveMember.mockResolvedValue(false);

const context = {
toolCallId: '1',
messages: [],
experimental_context: { userId: 'user-123' } as ToolExecutionContext,
};

await expect(
activityTools.get_activity.execute!(createTestInput({ driveIds: ['drive-1'] }), context)
).rejects.toThrow('No access to any of the specified drives');
});

it('has expected input schema shape', () => {
const schema = activityTools.get_activity.inputSchema;
expect(schema).toBeDefined();

// Verify schema is a Zod object with expected structure
// In Zod v4, use .type instead of ._def.typeName
const schemaType = (schema as { type?: string })?.type;
expect(schemaType).toBe('object');
});

it('description explains use cases', () => {
const desc = activityTools.get_activity.description;
expect(desc).toContain('activity');
expect(desc).toContain('workspace');
// Should mention key use cases
expect(desc).toMatch(/collaborat|pulse|welcome|context/i);
});
});
});
Loading