export-import.test.ts•21 kB
/**
* Advanced unit tests for Memory Export/Import System
* Tests data portability, backup, and migration capabilities
* Part of Issue #55 - Advanced Memory Components Unit Tests
*/
import { promises as fs } from "fs";
import path from "path";
import os from "os";
import { MemoryManager } from "../../src/memory/manager.js";
import { JSONLStorage } from "../../src/memory/storage.js";
import { IncrementalLearningSystem } from "../../src/memory/learning.js";
import { KnowledgeGraph } from "../../src/memory/knowledge-graph.js";
import {
MemoryExportImportSystem,
ExportOptions,
ImportOptions,
ExportResult,
ImportResult,
} from "../../src/memory/export-import.js";
describe("MemoryExportImportSystem", () => {
let tempDir: string;
let exportDir: string;
let memoryManager: MemoryManager;
let storage: JSONLStorage;
let learningSystem: IncrementalLearningSystem;
let knowledgeGraph: KnowledgeGraph;
let exportImportSystem: MemoryExportImportSystem;
beforeEach(async () => {
// Create unique temp directories for each test
tempDir = path.join(
os.tmpdir(),
`export-import-test-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`,
);
exportDir = path.join(tempDir, "exports");
await fs.mkdir(tempDir, { recursive: true });
await fs.mkdir(exportDir, { recursive: true });
memoryManager = new MemoryManager(tempDir);
await memoryManager.initialize();
// Use the memory manager's storage for consistency
storage = memoryManager.getStorage();
learningSystem = new IncrementalLearningSystem(memoryManager);
await learningSystem.initialize();
knowledgeGraph = new KnowledgeGraph(memoryManager);
await knowledgeGraph.initialize();
exportImportSystem = new MemoryExportImportSystem(
storage,
memoryManager,
learningSystem,
knowledgeGraph,
);
});
afterEach(async () => {
// Cleanup temp directories
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe("Export System", () => {
beforeEach(async () => {
// Set up test data for export tests
memoryManager.setContext({ projectId: "export-test-project" });
await memoryManager.remember(
"analysis",
{
language: { primary: "typescript" },
framework: { name: "react" },
metrics: { complexity: "medium", performance: "good" },
},
{
tags: ["frontend", "typescript"],
repository: "github.com/test/repo",
},
);
await memoryManager.remember(
"recommendation",
{
recommended: "docusaurus",
confidence: 0.9,
reasoning: ["typescript support", "react compatibility"],
},
{
tags: ["documentation", "ssg"],
},
);
await memoryManager.remember(
"deployment",
{
status: "success",
platform: "github-pages",
duration: 120,
url: "https://test.github.io",
},
{
tags: ["deployment", "success"],
},
);
});
test("should export memories in JSON format", async () => {
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
compression: "none",
};
const exportPath = path.join(exportDir, "test-export.json");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
expect(result).toBeDefined();
expect(result.success).toBe(true);
expect(result.entries).toBeGreaterThan(0);
expect(result.filePath).toBe(exportPath);
// Verify file was created
const fileExists = await fs
.access(exportPath)
.then(() => true)
.catch(() => false);
expect(fileExists).toBe(true);
// Verify file content
const content = await fs.readFile(exportPath, "utf-8");
const exported = JSON.parse(content);
expect(exported).toHaveProperty("metadata");
expect(exported).toHaveProperty("memories");
expect(Array.isArray(exported.memories)).toBe(true);
expect(exported.memories.length).toBe(3);
});
test("should export memories in JSONL format", async () => {
const exportOptions: ExportOptions = {
format: "jsonl",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
};
const exportPath = path.join(exportDir, "test-export.jsonl");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
expect(result.success).toBe(true);
expect(result.entries).toBe(3);
// Verify JSONL format
const content = await fs.readFile(exportPath, "utf-8");
const lines = content.trim().split("\n");
expect(lines.length).toBe(4); // 1 metadata + 3 memory entries
lines.forEach((line) => {
expect(() => JSON.parse(line)).not.toThrow();
});
// First line should be metadata
const firstLine = JSON.parse(lines[0]);
expect(firstLine).toHaveProperty("version");
expect(firstLine).toHaveProperty("exportedAt");
});
test("should export with filtering options", async () => {
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
filters: {
types: ["analysis", "recommendation"],
tags: ["frontend"],
},
};
const exportPath = path.join(exportDir, "filtered-export.json");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
expect(result.success).toBe(true);
const content = await fs.readFile(exportPath, "utf-8");
const exported = JSON.parse(content);
// Should only include filtered types
exported.memories.forEach((memory: any) => {
expect(["analysis", "recommendation"]).toContain(memory.type);
});
});
test("should handle compression options", async () => {
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
compression: "gzip",
};
const exportPath = path.join(exportDir, "compressed-export.json.gz");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
expect(result.success).toBe(true);
expect(result.metadata.compression).toBe("gzip");
// Verify compressed file exists
const fileExists = await fs
.access(exportPath)
.then(() => true)
.catch(() => false);
expect(fileExists).toBe(true);
});
test("should export with anonymization", async () => {
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
anonymize: {
enabled: true,
fields: ["repository", "url"],
method: "hash",
},
};
const exportPath = path.join(exportDir, "anonymized-export.json");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
expect(result.success).toBe(true);
const content = await fs.readFile(exportPath, "utf-8");
const exported = JSON.parse(content);
// Check that specified fields are anonymized
exported.memories.forEach((memory: any) => {
if (memory.metadata.repository) {
// Should be hashed, not original value
expect(memory.metadata.repository).not.toBe("github.com/test/repo");
}
if (memory.data.url) {
expect(memory.data.url).not.toBe("https://test.github.io");
}
});
});
});
describe("Import System", () => {
let testExportPath: string;
beforeEach(async () => {
// Create test export file for import tests
testExportPath = path.join(exportDir, "test-import.json");
const testData = {
metadata: {
exportedAt: new Date().toISOString(),
version: "1.0.0",
source: "test",
},
memories: [
{
id: "test-import-1",
type: "analysis",
timestamp: new Date().toISOString(),
data: {
language: { primary: "python" },
framework: { name: "django" },
},
metadata: {
projectId: "import-test-project",
tags: ["backend", "python"],
},
},
{
id: "test-import-2",
type: "recommendation",
timestamp: new Date().toISOString(),
data: {
recommended: "mkdocs",
confidence: 0.8,
},
metadata: {
projectId: "import-test-project",
tags: ["documentation"],
},
},
],
};
await fs.writeFile(testExportPath, JSON.stringify(testData, null, 2));
});
test("should import memories from JSON file", async () => {
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "strict",
conflictResolution: "skip",
backup: false,
dryRun: false,
};
const result = await exportImportSystem.importMemories(
testExportPath,
importOptions,
);
expect(result).toBeDefined();
expect(result.success).toBe(true);
expect(result.imported).toBe(2);
expect(result.skipped).toBe(0);
expect(result.errors).toBe(0);
// Verify memories were imported
const searchResults = await memoryManager.search("import-test-project");
expect(searchResults.length).toBeGreaterThanOrEqual(2);
});
test("should handle import conflicts", async () => {
// First import
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "loose",
conflictResolution: "skip",
backup: false,
dryRun: false,
};
await exportImportSystem.importMemories(testExportPath, importOptions);
// Second import with same data (should skip duplicates)
const result2 = await exportImportSystem.importMemories(
testExportPath,
importOptions,
);
expect(result2.success).toBe(true);
expect(result2.skipped).toBeGreaterThan(0);
});
test("should validate imported data", async () => {
// Create invalid test data
const invalidDataPath = path.join(exportDir, "invalid-import.json");
const invalidData = {
memories: [
{
// Missing required fields
type: "invalid",
data: null,
},
],
};
await fs.writeFile(invalidDataPath, JSON.stringify(invalidData));
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "strict",
conflictResolution: "skip",
backup: false,
dryRun: false,
};
const result = await exportImportSystem.importMemories(
invalidDataPath,
importOptions,
);
expect(result.success).toBe(false);
expect(result.errors).toBeGreaterThan(0);
expect(Array.isArray(result.errorDetails)).toBe(true);
expect(result.errorDetails.length).toBeGreaterThan(0);
});
test("should perform dry run import", async () => {
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "strict",
conflictResolution: "skip",
backup: false,
dryRun: true,
};
const result = await exportImportSystem.importMemories(
testExportPath,
importOptions,
);
expect(result.success).toBe(true);
// In dry run mode, nothing should be actually imported
expect(result.imported).toBe(0); // Nothing actually imported in dry run
// Verify no memories were actually imported
const searchResults = await memoryManager.search("import-test-project");
expect(searchResults.length).toBe(0);
});
test("should create backup before import", async () => {
// Add some existing data
memoryManager.setContext({ projectId: "existing-data" });
await memoryManager.remember("analysis", { existing: true });
const importOptions: ImportOptions = {
format: "json",
mode: "replace",
validation: "loose",
conflictResolution: "overwrite",
backup: true,
dryRun: false,
};
const result = await exportImportSystem.importMemories(
testExportPath,
importOptions,
);
expect(result.success).toBe(true);
// Backup creation is handled internally during import process
// Verify that the import was successful
expect(result.success).toBe(true);
});
});
describe("Data Migration and Transformation", () => {
test("should transform data during import", async () => {
const sourceDataPath = path.join(exportDir, "source-data.json");
const sourceData = {
memories: [
{
id: "transform-test-1",
type: "analysis",
timestamp: new Date().toISOString(),
data: {
// Old format
lang: "typescript",
fw: "react",
},
metadata: {
project: "transform-test",
},
},
],
};
await fs.writeFile(sourceDataPath, JSON.stringify(sourceData));
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "loose",
conflictResolution: "skip",
backup: false,
dryRun: false,
mapping: {
"data.lang": "data.language.primary",
"data.fw": "data.framework.name",
"metadata.project": "metadata.projectId",
},
transformation: {
enabled: true,
rules: [
{
field: "data.language.primary",
operation: "transform",
params: { value: "typescript" },
},
],
},
};
const result = await exportImportSystem.importMemories(
sourceDataPath,
importOptions,
);
expect(result.success).toBe(true);
// Transformation should result in successful import
expect(result.imported).toBeGreaterThan(0);
// Verify transformation worked
const imported = await memoryManager.search("transform-test");
expect(imported.length).toBe(1);
expect(imported[0].data.language?.primary).toBe("typescript");
expect(imported[0].data.framework?.name).toBe("react");
expect(imported[0].metadata.projectId).toBe("transform-test");
});
test("should migrate between different versions", async () => {
const oldVersionData = {
version: "0.1.0",
memories: [
{
id: "migration-test-1",
type: "analysis",
timestamp: new Date().toISOString(),
// Old schema
project: "migration-test",
language: "python",
recommendation: "mkdocs",
},
],
};
const migrationPath = path.join(exportDir, "migration-data.json");
await fs.writeFile(migrationPath, JSON.stringify(oldVersionData));
// Create a simple migration plan for testing
const migrationPlan = await exportImportSystem.createMigrationPlan(
{ system: "OldVersion", fields: {} },
{ system: "DocuMCP", fields: {} },
);
const result = await exportImportSystem.executeMigration(
migrationPath,
migrationPlan,
);
expect(result.success).toBe(true);
expect(result.imported).toBeGreaterThan(0);
// Verify migration created proper structure
const migrated = await memoryManager.search("migration-test");
expect(migrated.length).toBe(1);
expect(migrated[0]).toHaveProperty("data");
expect(migrated[0]).toHaveProperty("metadata");
});
});
describe("Bulk Operations and Performance", () => {
test("should handle large-scale export efficiently", async () => {
memoryManager.setContext({ projectId: "bulk-export-test" });
// Create many memories
const promises = Array.from({ length: 100 }, (_, i) =>
memoryManager.remember("analysis", {
index: i,
content: `bulk test content ${i}`,
}),
);
await Promise.all(promises);
const exportOptions: ExportOptions = {
format: "jsonl",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
};
const startTime = Date.now();
const exportPath = path.join(exportDir, "bulk-export.jsonl");
const result = await exportImportSystem.exportMemories(
exportPath,
exportOptions,
);
const exportTime = Date.now() - startTime;
expect(result.success).toBe(true);
expect(result.entries).toBe(100);
expect(exportTime).toBeLessThan(10000); // Should complete within 10 seconds
});
test("should provide progress updates for long operations", async () => {
memoryManager.setContext({ projectId: "progress-test" });
// Add test data
await memoryManager.remember("analysis", { progressTest: true });
const progressUpdates: number[] = [];
exportImportSystem.on("export-progress", (progress: number) => {
progressUpdates.push(progress);
});
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
};
const exportPath = path.join(exportDir, "progress-export.json");
await exportImportSystem.exportMemories(exportPath, exportOptions);
// Progress updates might not be generated for small datasets
expect(Array.isArray(progressUpdates)).toBe(true);
});
});
describe("Error Handling and Recovery", () => {
test("should handle file system errors gracefully", async () => {
const invalidPath = "/invalid/path/that/does/not/exist/export.json";
const exportOptions: ExportOptions = {
format: "json",
includeMetadata: true,
includeLearning: false,
includeKnowledgeGraph: false,
};
const result = await exportImportSystem.exportMemories(
invalidPath,
exportOptions,
);
expect(result.success).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
test("should recover from partial import failures", async () => {
const partialDataPath = path.join(exportDir, "partial-data.json");
const partialData = {
memories: [
{
id: "valid-memory",
type: "analysis",
timestamp: new Date().toISOString(),
data: { valid: true },
metadata: { projectId: "partial-test" },
},
{
// Invalid memory
id: "invalid-memory",
type: null,
data: null,
},
],
};
await fs.writeFile(partialDataPath, JSON.stringify(partialData));
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "loose",
conflictResolution: "skip",
backup: false,
dryRun: false,
};
const result = await exportImportSystem.importMemories(
partialDataPath,
importOptions,
);
expect(result.imported).toBe(1); // Only valid memory imported
expect(result.errors).toBe(1); // One error for invalid memory
expect(Array.isArray(result.errorDetails)).toBe(true);
expect(result.errorDetails.length).toBe(1);
});
test("should validate data integrity", async () => {
const corruptDataPath = path.join(exportDir, "corrupt-data.json");
await fs.writeFile(corruptDataPath, "{ invalid json");
const importOptions: ImportOptions = {
format: "json",
mode: "append",
validation: "strict",
conflictResolution: "skip",
backup: false,
dryRun: false,
};
const result = await exportImportSystem.importMemories(
corruptDataPath,
importOptions,
);
expect(result.success).toBe(false);
expect(result.errors).toBeGreaterThan(0);
});
});
});