282345f379
This is a backup of all work done by the 3 Phase 1 agents: Agent A - Command Injection Fix (Issue #354): - Fixed command injection in BranchManager.ts - Fixed unnecessary shell usage in bun-path.ts - Added comprehensive security test suite - Created SECURITY.md and SECURITY_AUDIT_REPORT.md Agent B - Observation Persistence Fix (Issue #353): - Added PendingMessageStore from PR #335 - Integrated persistent queue into SessionManager - Modified SDKAgent to mark messages complete - Updated SessionStore with pending_messages migration - Updated worker-types.ts with new interfaces Agent C - Batch Endpoint Verification (Issue #348): - Created batch-observations.test.ts - Updated worker-service.mdx documentation Also includes: - Documentation context files (biomimetic, windows struggles) - Build artifacts from agent testing This work will be re-evaluated after v7.3.0 release.
249 lines
6.5 KiB
TypeScript
249 lines
6.5 KiB
TypeScript
/**
|
|
* Happy Path Test: Batch Observations Endpoint
|
|
*
|
|
* Tests that the batch observations endpoint correctly retrieves
|
|
* multiple observations by their IDs in a single request.
|
|
*/
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { getWorkerPort } from '../../src/shared/worker-utils.js';
|
|
|
|
describe('Batch Observations Endpoint', () => {
|
|
const WORKER_PORT = getWorkerPort();
|
|
const WORKER_BASE_URL = `http://127.0.0.1:${WORKER_PORT}`;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('retrieves multiple observations by IDs', async () => {
|
|
// Mock response with multiple observations
|
|
const mockObservations = [
|
|
{
|
|
id: 1,
|
|
sdk_session_id: 'test-session-1',
|
|
project: 'test-project',
|
|
type: 'discovery',
|
|
title: 'Test Discovery 1',
|
|
created_at: '2024-01-01T10:00:00Z',
|
|
created_at_epoch: 1704103200000
|
|
},
|
|
{
|
|
id: 2,
|
|
sdk_session_id: 'test-session-2',
|
|
project: 'test-project',
|
|
type: 'bugfix',
|
|
title: 'Test Bugfix',
|
|
created_at: '2024-01-02T10:00:00Z',
|
|
created_at_epoch: 1704189600000
|
|
},
|
|
{
|
|
id: 3,
|
|
sdk_session_id: 'test-session-3',
|
|
project: 'test-project',
|
|
type: 'feature',
|
|
title: 'Test Feature',
|
|
created_at: '2024-01-03T10:00:00Z',
|
|
created_at_epoch: 1704276000000
|
|
}
|
|
];
|
|
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockObservations
|
|
});
|
|
|
|
// Execute: Fetch observations by IDs
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ ids: [1, 2, 3] })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Response contains all requested observations
|
|
expect(response.ok).toBe(true);
|
|
expect(data).toHaveLength(3);
|
|
expect(data[0].id).toBe(1);
|
|
expect(data[1].id).toBe(2);
|
|
expect(data[2].id).toBe(3);
|
|
});
|
|
|
|
it('applies orderBy parameter correctly', async () => {
|
|
const mockObservations = [
|
|
{
|
|
id: 3,
|
|
created_at: '2024-01-03T10:00:00Z',
|
|
created_at_epoch: 1704276000000
|
|
},
|
|
{
|
|
id: 2,
|
|
created_at: '2024-01-02T10:00:00Z',
|
|
created_at_epoch: 1704189600000
|
|
},
|
|
{
|
|
id: 1,
|
|
created_at: '2024-01-01T10:00:00Z',
|
|
created_at_epoch: 1704103200000
|
|
}
|
|
];
|
|
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockObservations
|
|
});
|
|
|
|
// Execute: Fetch with date_desc ordering
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
ids: [1, 2, 3],
|
|
orderBy: 'date_desc'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Results are ordered by date descending
|
|
expect(data[0].id).toBe(3);
|
|
expect(data[1].id).toBe(2);
|
|
expect(data[2].id).toBe(1);
|
|
});
|
|
|
|
it('applies limit parameter correctly', async () => {
|
|
const mockObservations = [
|
|
{ id: 3, created_at_epoch: 1704276000000 },
|
|
{ id: 2, created_at_epoch: 1704189600000 }
|
|
];
|
|
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockObservations
|
|
});
|
|
|
|
// Execute: Fetch with limit=2
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
ids: [1, 2, 3],
|
|
limit: 2
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Only 2 results returned
|
|
expect(data).toHaveLength(2);
|
|
});
|
|
|
|
it('filters by project parameter', async () => {
|
|
const mockObservations = [
|
|
{ id: 1, project: 'project-a' },
|
|
{ id: 2, project: 'project-a' }
|
|
];
|
|
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockObservations
|
|
});
|
|
|
|
// Execute: Fetch with project filter
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
ids: [1, 2, 3],
|
|
project: 'project-a'
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Only matching project observations returned
|
|
expect(data).toHaveLength(2);
|
|
expect(data.every((obs: any) => obs.project === 'project-a')).toBe(true);
|
|
});
|
|
|
|
it('returns empty array for empty IDs', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => []
|
|
});
|
|
|
|
// Execute: Fetch with empty IDs array
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ ids: [] })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Empty array returned
|
|
expect(data).toEqual([]);
|
|
});
|
|
|
|
it('returns error for invalid IDs parameter', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 400,
|
|
json: async () => ({ error: 'ids must be an array of numbers' })
|
|
});
|
|
|
|
// Execute: Fetch with invalid IDs (string instead of array)
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ ids: 'not-an-array' })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Error response returned
|
|
expect(response.ok).toBe(false);
|
|
expect(data.error).toBe('ids must be an array of numbers');
|
|
});
|
|
|
|
it('returns error for non-integer IDs', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 400,
|
|
json: async () => ({ error: 'All ids must be integers' })
|
|
});
|
|
|
|
// Execute: Fetch with mixed types in IDs array
|
|
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ ids: [1, 'two', 3] })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Verify: Error response returned
|
|
expect(response.ok).toBe(false);
|
|
expect(data.error).toBe('All ids must be integers');
|
|
});
|
|
});
|