mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
312 lines
9.4 KiB
TypeScript
312 lines
9.4 KiB
TypeScript
/**
|
|
* Pull Sync Agent Unit Tests
|
|
*
|
|
* Tests for cloud → local pull orchestration:
|
|
* - Full pull workflow
|
|
* - Incremental pull workflow
|
|
* - Dry-run mode (no writes)
|
|
* - Table filtering
|
|
* - Error handling and partial results
|
|
* - Verify (cloud vs local count comparison)
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock all external dependencies before importing
|
|
vi.mock('uuid', () => ({ v4: () => 'test-uuid-1234' }));
|
|
// Track mock state
|
|
let mockCloudRecords: Record<string, Record<string, unknown>[]> = {};
|
|
let mockLocalCounts: Record<string, number> = {};
|
|
let mockLocalWritten: Record<string, number> = {};
|
|
|
|
// Mock the cloud connection modules — paths relative to the source file that imports them
|
|
vi.mock('../../../src/sync/cloud/tunnel-manager.js', () => ({
|
|
createConnectionManager: vi.fn(() => ({
|
|
start: vi.fn().mockResolvedValue({ host: 'localhost', port: 15432, startedAt: new Date() }),
|
|
stop: vi.fn(),
|
|
isActive: vi.fn().mockReturnValue(true),
|
|
getConnection: vi.fn().mockReturnValue({ host: 'localhost', port: 15432, startedAt: new Date() }),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('../../../src/sync/cloud/postgres-writer.js', () => ({
|
|
createPostgresWriter: vi.fn(() => ({
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
query: vi.fn().mockResolvedValue([]),
|
|
beginTransaction: vi.fn(),
|
|
commit: vi.fn(),
|
|
rollback: vi.fn(),
|
|
upsert: vi.fn().mockResolvedValue(0),
|
|
execute: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('../../../src/sync/cloud/postgres-reader.js', () => ({
|
|
PostgresReader: vi.fn(),
|
|
createPostgresReader: vi.fn(() => ({
|
|
readAll: vi.fn().mockImplementation(async (source: { cloudTable: string }) => {
|
|
return mockCloudRecords[source.cloudTable] || [];
|
|
}),
|
|
readChanged: vi.fn().mockImplementation(async (source: { cloudTable: string }) => {
|
|
return mockCloudRecords[source.cloudTable] || [];
|
|
}),
|
|
count: vi.fn().mockImplementation(async (source: { cloudTable: string }) => {
|
|
const records = mockCloudRecords[source.cloudTable];
|
|
return records ? records.length : 0;
|
|
}),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('../../../src/sync/writers/sqlite-writer.js', () => ({
|
|
SQLiteWriter: vi.fn(),
|
|
createSQLiteWriter: vi.fn(() => ({
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
upsert: vi.fn().mockImplementation(async (table: string, records: unknown[]) => {
|
|
const written = records.length;
|
|
mockLocalWritten[table] = (mockLocalWritten[table] || 0) + written;
|
|
return written;
|
|
}),
|
|
count: vi.fn().mockImplementation(async (table: string) => {
|
|
return mockLocalCounts[table] || mockLocalWritten[table] || 0;
|
|
}),
|
|
})),
|
|
}));
|
|
|
|
// Mock fs for backup check (existsSync returns true, copyFileSync is a no-op)
|
|
vi.mock('fs', async () => {
|
|
const actual = await vi.importActual<typeof import('fs')>('fs');
|
|
return {
|
|
...actual,
|
|
existsSync: vi.fn().mockReturnValue(true),
|
|
copyFileSync: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import {
|
|
PullSyncAgent,
|
|
createPullSyncAgent,
|
|
pullFromCloud,
|
|
type PullAgentConfig,
|
|
} from '../../../src/sync/pull-agent.js';
|
|
import type { PullSource } from '../../../src/sync/interfaces.js';
|
|
|
|
describe('PullSyncAgent', () => {
|
|
const testSources: PullSource[] = [
|
|
{
|
|
name: 'goap-actions',
|
|
cloudTable: 'aqe.goap_actions',
|
|
localTable: 'goap_actions',
|
|
enabled: true,
|
|
priority: 'high',
|
|
mode: 'incremental',
|
|
dropColumns: ['source_env'],
|
|
},
|
|
{
|
|
name: 'dream-insights',
|
|
cloudTable: 'aqe.dream_insights',
|
|
localTable: 'dream_insights',
|
|
enabled: true,
|
|
priority: 'low',
|
|
mode: 'append',
|
|
dropColumns: ['source_env'],
|
|
},
|
|
{
|
|
name: 'disabled-source',
|
|
cloudTable: 'aqe.disabled',
|
|
localTable: 'disabled',
|
|
enabled: false,
|
|
priority: 'low',
|
|
mode: 'full',
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockCloudRecords = {};
|
|
mockLocalCounts = {};
|
|
mockLocalWritten = {};
|
|
});
|
|
|
|
describe('pullAll', () => {
|
|
it('should pull all enabled sources and report results', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.goap_actions': [
|
|
{ id: '1', name: 'action1' },
|
|
{ id: '2', name: 'action2' },
|
|
],
|
|
'aqe.dream_insights': [
|
|
{ id: 'd1', insight: 'test' },
|
|
],
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
expect(report.status).toBe('completed');
|
|
expect(report.results).toHaveLength(2); // Only enabled sources
|
|
expect(report.totalRecordsSynced).toBe(3);
|
|
expect(report.results[0].source).toBe('goap-actions'); // high priority first
|
|
expect(report.results[0].recordsSynced).toBe(2);
|
|
expect(report.results[1].source).toBe('dream-insights'); // low priority second
|
|
expect(report.results[1].recordsSynced).toBe(1);
|
|
});
|
|
|
|
it('should skip disabled sources', async () => {
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
const sourceNames = report.results.map(r => r.source);
|
|
expect(sourceNames).not.toContain('disabled-source');
|
|
});
|
|
|
|
it('should report 0 records for empty cloud tables', async () => {
|
|
mockCloudRecords = {};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
expect(report.status).toBe('completed');
|
|
expect(report.totalRecordsSynced).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('dry run', () => {
|
|
it('should not write to local DB in dry-run mode', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.goap_actions': [{ id: '1', name: 'action1' }],
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
dryRun: true,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
expect(report.status).toBe('completed');
|
|
expect(report.results[0].recordsSynced).toBe(1); // Counted but not written
|
|
expect(mockLocalWritten).toEqual({}); // Nothing actually written
|
|
});
|
|
});
|
|
|
|
describe('table filtering', () => {
|
|
it('should only pull specified tables', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.goap_actions': [{ id: '1' }],
|
|
'aqe.dream_insights': [{ id: 'd1' }],
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
tables: ['goap_actions'],
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
expect(report.results).toHaveLength(1);
|
|
expect(report.results[0].table).toBe('goap_actions');
|
|
});
|
|
|
|
it('should support filtering by source name', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.dream_insights': [{ id: 'd1' }],
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
tables: ['dream-insights'], // by name, not table
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
expect(report.results).toHaveLength(1);
|
|
expect(report.results[0].source).toBe('dream-insights');
|
|
});
|
|
});
|
|
|
|
describe('verify', () => {
|
|
it('should compare cloud and local counts', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.goap_actions': [{ id: '1' }, { id: '2' }],
|
|
'aqe.dream_insights': [{ id: 'd1' }],
|
|
};
|
|
mockLocalCounts = {
|
|
'goap_actions': 2,
|
|
'dream_insights': 0,
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const result = await agent.verify();
|
|
await agent.close();
|
|
|
|
expect(result.results).toHaveLength(2);
|
|
const goap = result.results.find(r => r.localTable === 'goap_actions')!;
|
|
expect(goap.match).toBe(true);
|
|
expect(goap.cloudCount).toBe(2);
|
|
expect(goap.localCount).toBe(2);
|
|
|
|
const dreams = result.results.find(r => r.localTable === 'dream_insights')!;
|
|
expect(dreams.match).toBe(false);
|
|
expect(dreams.diff).toBe(-1); // local 0 - cloud 1
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should throw if not initialized', async () => {
|
|
const agent = createPullSyncAgent({ sources: testSources });
|
|
// pullAll without initialize
|
|
const report = await agent.pullAll();
|
|
expect(report.status).toBe('failed');
|
|
expect(report.errors).toHaveLength(1);
|
|
expect(report.errors[0]).toContain('not initialized');
|
|
});
|
|
});
|
|
|
|
describe('priority ordering', () => {
|
|
it('should process high priority sources before low', async () => {
|
|
mockCloudRecords = {
|
|
'aqe.goap_actions': [{ id: '1' }],
|
|
'aqe.dream_insights': [{ id: 'd1' }],
|
|
};
|
|
|
|
const agent = createPullSyncAgent({
|
|
sources: testSources,
|
|
verbose: false,
|
|
});
|
|
await agent.initialize();
|
|
const report = await agent.pullAll();
|
|
await agent.close();
|
|
|
|
// high priority (goap-actions) should come before low (dream-insights)
|
|
expect(report.results[0].source).toBe('goap-actions');
|
|
expect(report.results[1].source).toBe('dream-insights');
|
|
});
|
|
});
|
|
});
|