mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-20 07:37:26 -05:00
- Added a new test suite in `core/tests/typescript_bridge_test.rs` to facilitate end-to-end testing between Rust and TypeScript. - Introduced `IndexingHarnessBuilder` enhancements to support a daemon RPC server for TypeScript tests. - Created TypeScript test files for `useNormalizedQuery` to validate file move and folder rename operations. - Configured Bun test environment with `bunfig.toml` and setup scripts for improved testing efficiency. - Updated `TcpSocketTransport` to handle TCP connections for the TypeScript client, ensuring robust communication with the Rust backend. - Enhanced logging and error handling for better visibility during test execution.
255 lines
7.3 KiB
TypeScript
255 lines
7.3 KiB
TypeScript
/**
|
|
* TypeScript Integration Test: useNormalizedQuery with File Moves
|
|
*
|
|
* This test is spawned by a Rust test harness that provides:
|
|
* - Real Spacedrive daemon running on Unix socket
|
|
* - Indexed location with test files
|
|
* - Connection configuration via BRIDGE_CONFIG_PATH env var
|
|
*
|
|
* Test flow:
|
|
* 1. Connect to daemon using bridge config
|
|
* 2. Query directory listing with useNormalizedQuery
|
|
* 3. Move files in filesystem
|
|
* 4. Verify cache updates correctly via WebSocket events
|
|
*/
|
|
|
|
// Setup DOM environment before any other imports
|
|
import "./setup";
|
|
|
|
import {
|
|
describe,
|
|
test,
|
|
expect,
|
|
beforeAll,
|
|
afterAll,
|
|
afterEach,
|
|
} from "bun:test";
|
|
import { readFile } from "fs/promises";
|
|
import { rename } from "fs/promises";
|
|
import { join } from "path";
|
|
import { hostname } from "os";
|
|
import { SpacedriveClient } from "../../src/client";
|
|
import { renderHook, waitFor, cleanup } from "@testing-library/react";
|
|
import { SpacedriveProvider } from "../../src/hooks/useClient";
|
|
import { useNormalizedQuery } from "../../src/hooks/useNormalizedQuery";
|
|
import React from "react";
|
|
|
|
// Bridge configuration from Rust test harness
|
|
interface BridgeConfig {
|
|
socket_addr: string;
|
|
library_id: string;
|
|
location_db_id: number;
|
|
location_path: string;
|
|
test_data_path: string;
|
|
}
|
|
|
|
let bridgeConfig: BridgeConfig;
|
|
let client: SpacedriveClient;
|
|
const allEventsReceived: any[] = []; // Collect all events for debugging
|
|
|
|
beforeAll(async () => {
|
|
// Read bridge config from path provided by Rust test
|
|
const configPath = process.env.BRIDGE_CONFIG_PATH;
|
|
if (!configPath) {
|
|
throw new Error("BRIDGE_CONFIG_PATH environment variable not set");
|
|
}
|
|
|
|
console.log(`[TS] Reading bridge config from: ${configPath}`);
|
|
const configJson = await readFile(configPath, "utf-8");
|
|
bridgeConfig = JSON.parse(configJson);
|
|
|
|
console.log(`[TS] Bridge config:`, bridgeConfig);
|
|
|
|
// Connect to daemon via TCP socket
|
|
client = SpacedriveClient.fromTcpSocket(bridgeConfig.socket_addr);
|
|
|
|
console.log(`[TS] Connected to daemon`);
|
|
|
|
// Set library context
|
|
client.setCurrentLibrary(bridgeConfig.library_id);
|
|
console.log(`[TS] Library set to: ${bridgeConfig.library_id}`);
|
|
|
|
// Hook into the subscription manager to collect all events
|
|
const originalCreateSubscription = (client as any).subscriptionManager
|
|
.createSubscription;
|
|
(client as any).subscriptionManager.createSubscription = function (
|
|
filter: any,
|
|
callback: any,
|
|
) {
|
|
const wrappedCallback = (event: any) => {
|
|
allEventsReceived.push({
|
|
timestamp: new Date().toISOString(),
|
|
filter,
|
|
event,
|
|
});
|
|
console.log(
|
|
`[TS] 🔔 Event received:`,
|
|
JSON.stringify(event, null, 2),
|
|
);
|
|
callback(event);
|
|
};
|
|
return originalCreateSubscription.call(this, filter, wrappedCallback);
|
|
};
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Log all events at the end for debugging
|
|
console.log(
|
|
`[TS] ===== ALL EVENTS RECEIVED (${allEventsReceived.length}) =====`,
|
|
);
|
|
allEventsReceived.forEach((item, idx) => {
|
|
console.log(`[TS] Event ${idx + 1} at ${item.timestamp}:`);
|
|
console.log(`[TS] Filter:`, JSON.stringify(item.filter, null, 2));
|
|
console.log(`[TS] Event:`, JSON.stringify(item.event, null, 2));
|
|
});
|
|
console.log(`[TS] ===== END OF EVENTS =====`);
|
|
// No explicit disconnect needed for stateless transports
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up React components after each test
|
|
cleanup();
|
|
});
|
|
|
|
describe("useNormalizedQuery - File Moves Integration", () => {
|
|
test("should update cache when file moves between folders", async () => {
|
|
const folderAPath = join(bridgeConfig.location_path, "folder_a");
|
|
const folderBPath = join(bridgeConfig.location_path, "folder_b");
|
|
|
|
// Get device slug from hostname
|
|
const deviceSlug = hostname().toLowerCase().replace(/\s+/g, "-");
|
|
|
|
// Create wrapper for React hooks with SpacedriveProvider
|
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(SpacedriveProvider, { client }, children);
|
|
|
|
// Query folder_a listing
|
|
const { result: folderAResult } = renderHook(
|
|
() =>
|
|
useNormalizedQuery({
|
|
wireMethod: "query:files.directory_listing",
|
|
input: {
|
|
path: {
|
|
Physical: {
|
|
device_slug: deviceSlug,
|
|
path: folderAPath,
|
|
},
|
|
},
|
|
sort_by: "name",
|
|
},
|
|
resourceType: "file",
|
|
pathScope: {
|
|
Physical: {
|
|
device_slug: deviceSlug,
|
|
path: folderAPath,
|
|
},
|
|
},
|
|
includeDescendants: false,
|
|
debug: true,
|
|
}),
|
|
{ wrapper },
|
|
);
|
|
|
|
// Query folder_b listing
|
|
const { result: folderBResult } = renderHook(
|
|
() =>
|
|
useNormalizedQuery({
|
|
wireMethod: "query:files.directory_listing",
|
|
input: {
|
|
path: {
|
|
Physical: {
|
|
device_slug: deviceSlug,
|
|
path: folderBPath,
|
|
},
|
|
},
|
|
sort_by: "name",
|
|
},
|
|
resourceType: "file",
|
|
pathScope: {
|
|
Physical: {
|
|
device_slug: deviceSlug,
|
|
path: folderBPath,
|
|
},
|
|
},
|
|
includeDescendants: false,
|
|
debug: true,
|
|
}),
|
|
{ wrapper },
|
|
);
|
|
|
|
// Wait for initial data
|
|
await waitFor(
|
|
() => {
|
|
console.log("[TS] Waiting for data...", {
|
|
folderA: {
|
|
data: folderAResult.current.data,
|
|
error: folderAResult.current.error,
|
|
isLoading: folderAResult.current.isLoading,
|
|
},
|
|
folderB: {
|
|
data: folderBResult.current.data,
|
|
error: folderBResult.current.error,
|
|
isLoading: folderBResult.current.isLoading,
|
|
},
|
|
});
|
|
expect(folderAResult.current.data).toBeDefined();
|
|
expect(folderBResult.current.data).toBeDefined();
|
|
},
|
|
{ timeout: 5000 },
|
|
);
|
|
|
|
console.log("[TS] Initial folder_a files:", folderAResult.current.data);
|
|
console.log("[TS] Initial folder_b files:", folderBResult.current.data);
|
|
|
|
// Verify initial state
|
|
const initialFolderAData = folderAResult.current.data as {
|
|
files: any[];
|
|
};
|
|
const initialFolderBData = folderBResult.current.data as {
|
|
files: any[];
|
|
};
|
|
|
|
expect(initialFolderAData.files.length).toBeGreaterThanOrEqual(2); // file1.txt, file2.rs
|
|
expect(initialFolderBData.files.length).toBeGreaterThanOrEqual(1); // file3.md
|
|
|
|
const file1Before = initialFolderAData.files.find(
|
|
(f: any) => f.name === "file1",
|
|
);
|
|
expect(file1Before).toBeDefined();
|
|
|
|
// Move file1.txt from folder_a to folder_b
|
|
console.log("[TS] Moving file1.txt from folder_a to folder_b");
|
|
await rename(
|
|
join(folderAPath, "file1.txt"),
|
|
join(folderBPath, "file1.txt"),
|
|
);
|
|
|
|
// Wait for watcher to detect and emit events (watcher buffers for 500ms + tick time)
|
|
await new Promise((resolve) => setTimeout(resolve, 8000));
|
|
|
|
// Verify cache updated correctly
|
|
const finalFolderAData = folderAResult.current.data as { files: any[] };
|
|
const finalFolderBData = folderBResult.current.data as { files: any[] };
|
|
|
|
console.log("[TS] Final folder_a files:", finalFolderAData);
|
|
console.log("[TS] Final folder_b files:", finalFolderBData);
|
|
|
|
// file1 should no longer be in folder_a
|
|
const file1InFolderA = finalFolderAData.files.find(
|
|
(f: any) => f.name === "file1",
|
|
);
|
|
expect(file1InFolderA).toBeUndefined();
|
|
|
|
// file1 should now be in folder_b
|
|
const file1InFolderB = finalFolderBData.files.find(
|
|
(f: any) => f.name === "file1",
|
|
);
|
|
expect(file1InFolderB).toBeDefined();
|
|
|
|
// UUID should be preserved (move detection)
|
|
expect(file1InFolderB.uuid).toBe(file1Before.uuid);
|
|
|
|
console.log("[TS] ✓ File move detected and cache updated correctly");
|
|
}, 30000); // 30s timeout for watcher delays
|
|
});
|