@photostructure/fs-metadata 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/CLAUDE.md +160 -136
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +2 -2
  5. package/README.md +34 -84
  6. package/binding.gyp +98 -23
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +53 -22
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +5 -0
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.mjs +52 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +505 -0
  17. package/doc/MACOS_API_REFERENCE.md +469 -0
  18. package/doc/SECURITY_AUDIT_2025.md +809 -0
  19. package/doc/SSH_RELEASE_HOWTO.md +207 -0
  20. package/doc/WINDOWS_API_REFERENCE.md +422 -0
  21. package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
  22. package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
  23. package/doc/examples.md +267 -0
  24. package/doc/gotchas.md +297 -0
  25. package/doc/logo.png +0 -0
  26. package/doc/logo.svg +85 -0
  27. package/doc/macos-asan-sip-issue.md +71 -0
  28. package/doc/social.png +0 -0
  29. package/doc/social.svg +125 -0
  30. package/doc/windows-build.md +226 -0
  31. package/doc/windows-clang-tidy.md +72 -0
  32. package/doc/windows-memory-testing.md +108 -0
  33. package/doc/windows-prebuildify-arm64.md +232 -0
  34. package/jest.config.cjs +24 -0
  35. package/package.json +68 -44
  36. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  37. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  38. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  39. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  40. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  41. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  42. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  43. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  44. package/scripts/check-memory.ts +186 -0
  45. package/scripts/clang-tidy.ts +832 -0
  46. package/scripts/install.cjs +42 -0
  47. package/scripts/is-platform.mjs +1 -1
  48. package/scripts/macos-asan.sh +155 -0
  49. package/scripts/post-build.mjs +3 -3
  50. package/scripts/prebuild-linux-glibc.sh +119 -0
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +70 -0
  53. package/scripts/sanitizers-test.sh +7 -1
  54. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  55. package/src/binding.cpp +1 -1
  56. package/src/common/error_utils.h +0 -6
  57. package/src/common/volume_metadata.h +6 -0
  58. package/src/darwin/hidden.cpp +73 -25
  59. package/src/darwin/path_security.h +149 -0
  60. package/src/darwin/raii_utils.h +104 -4
  61. package/src/darwin/volume_metadata.cpp +132 -58
  62. package/src/darwin/volume_mount_points.cpp +80 -47
  63. package/src/hidden.ts +36 -13
  64. package/src/linux/gio_mount_points.cpp +17 -18
  65. package/src/linux/gio_utils.cpp +92 -37
  66. package/src/linux/gio_utils.h +11 -5
  67. package/src/linux/gio_volume_metadata.cpp +111 -48
  68. package/src/linux/volume_metadata.cpp +67 -4
  69. package/src/object.ts +1 -0
  70. package/src/options.ts +6 -0
  71. package/src/path.ts +11 -0
  72. package/src/platform.ts +25 -0
  73. package/src/remote_info.ts +5 -3
  74. package/src/stack_path.ts +8 -6
  75. package/src/string_enum.ts +1 -0
  76. package/src/test-utils/benchmark-harness.ts +192 -0
  77. package/src/test-utils/debuglog-child.ts +30 -2
  78. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  79. package/src/test-utils/jest-setup.ts +14 -0
  80. package/src/test-utils/memory-test-core.ts +336 -0
  81. package/src/test-utils/memory-test-runner.ts +108 -0
  82. package/src/test-utils/platform.ts +46 -1
  83. package/src/test-utils/worker-thread-helper.cjs +157 -26
  84. package/src/types/native_bindings.ts +1 -1
  85. package/src/types/options.ts +6 -0
  86. package/src/windows/drive_status.h +133 -163
  87. package/src/windows/error_utils.h +54 -3
  88. package/src/windows/fs_meta.h +1 -1
  89. package/src/windows/hidden.cpp +60 -43
  90. package/src/windows/security_utils.h +250 -0
  91. package/src/windows/string.h +68 -11
  92. package/src/windows/system_volume.h +1 -1
  93. package/src/windows/thread_pool.h +206 -0
  94. package/src/windows/volume_metadata.cpp +11 -6
  95. package/src/windows/volume_mount_points.cpp +8 -7
  96. package/src/windows/windows_arch.h +39 -0
  97. package/scripts/check-memory.mjs +0 -123
  98. package/scripts/clang-tidy.mjs +0 -73
@@ -0,0 +1,192 @@
1
+ import { getTimingMultiplier } from "./test-timeout-config";
2
+
3
+ export interface BenchmarkOptions {
4
+ /**
5
+ * Target duration for the benchmark in milliseconds (default: 20000ms / 20 seconds)
6
+ */
7
+ targetDurationMs?: number;
8
+
9
+ /**
10
+ * Maximum timeout for the entire benchmark in milliseconds (default: 60000ms / 1 minute)
11
+ */
12
+ maxTimeoutMs?: number;
13
+
14
+ /**
15
+ * Minimum iterations to run regardless of timing (default: 5)
16
+ */
17
+ minIterations?: number;
18
+
19
+ /**
20
+ * Maximum iterations to run regardless of timing (default: 10000)
21
+ */
22
+ maxIterations?: number;
23
+
24
+ /**
25
+ * Number of warmup iterations before timing (default: 2)
26
+ */
27
+ warmupIterations?: number;
28
+
29
+ /**
30
+ * Whether to log debug information (default: false)
31
+ */
32
+ debug?: boolean;
33
+ }
34
+
35
+ export interface BenchmarkResult {
36
+ /**
37
+ * Number of iterations actually performed
38
+ */
39
+ iterations: number;
40
+
41
+ /**
42
+ * Total duration in milliseconds
43
+ */
44
+ totalDurationMs: number;
45
+
46
+ /**
47
+ * Average duration per iteration in milliseconds
48
+ */
49
+ avgIterationMs: number;
50
+
51
+ /**
52
+ * Whether the benchmark hit the timeout
53
+ */
54
+ timedOut: boolean;
55
+ }
56
+
57
+ /**
58
+ * Runs a benchmark operation adaptively based on the performance of the test environment.
59
+ *
60
+ * This harness:
61
+ * 1. Runs warmup iterations to estimate operation time
62
+ * 2. Calculates how many iterations can fit within the target duration
63
+ * 3. Runs the calculated number of iterations with a safety timeout
64
+ *
65
+ * @param operation - The async function to benchmark (should be a single iteration)
66
+ * @param options - Configuration options for the benchmark
67
+ * @returns Results of the benchmark run
68
+ */
69
+ export async function runAdaptiveBenchmark(
70
+ operation: () => Promise<void>,
71
+ options: BenchmarkOptions = {},
72
+ ): Promise<BenchmarkResult> {
73
+ const {
74
+ targetDurationMs = 20_000,
75
+ maxTimeoutMs = 60_000,
76
+ minIterations = 5,
77
+ maxIterations = 10_000,
78
+ warmupIterations = 2,
79
+ } = options;
80
+
81
+ // Apply timing multiplier based on environment
82
+ const multiplier = getTimingMultiplier();
83
+ const adjustedTargetMs = targetDurationMs * multiplier;
84
+ const adjustedTimeoutMs = maxTimeoutMs * multiplier;
85
+
86
+ // Debug logging removed to prevent 'Cannot log after tests are done' errors
87
+
88
+ // Run warmup iterations
89
+
90
+ const warmupStart = Date.now();
91
+ for (let i = 0; i < warmupIterations; i++) {
92
+ await operation();
93
+ }
94
+ const warmupDuration = Date.now() - warmupStart;
95
+ const avgWarmupTime = warmupDuration / warmupIterations;
96
+
97
+ // Warmup timing debug info removed to prevent console logging issues
98
+
99
+ // Calculate target iterations based on warmup timing
100
+ // Add 10% safety margin to avoid overshooting
101
+ const safetyMargin = 0.9;
102
+ let targetIterations = Math.floor(
103
+ (adjustedTargetMs * safetyMargin) / avgWarmupTime,
104
+ );
105
+
106
+ // Clamp to min/max bounds
107
+ targetIterations = Math.max(
108
+ minIterations,
109
+ Math.min(maxIterations, targetIterations),
110
+ );
111
+
112
+ // Target iterations debug info removed to prevent console logging issues
113
+
114
+ // Set up timeout promise
115
+ let timeoutHandle: NodeJS.Timeout | undefined;
116
+ const timeoutPromise = new Promise<void>((_, reject) => {
117
+ timeoutHandle = setTimeout(() => {
118
+ reject(new Error(`Benchmark timeout after ${adjustedTimeoutMs}ms`));
119
+ }, adjustedTimeoutMs);
120
+ });
121
+
122
+ // Run the actual benchmark
123
+ const benchmarkStart = Date.now();
124
+ let completedIterations = 0;
125
+ let timedOut = false;
126
+
127
+ try {
128
+ // Run iterations with timeout protection
129
+ await Promise.race([
130
+ (async () => {
131
+ for (let i = 0; i < targetIterations; i++) {
132
+ await operation();
133
+ completedIterations++;
134
+
135
+ // Check if we're approaching the timeout
136
+ const elapsed = Date.now() - benchmarkStart;
137
+ if (elapsed > adjustedTimeoutMs * 0.95) {
138
+ // Approaching timeout - stopping early
139
+ break;
140
+ }
141
+ }
142
+ })(),
143
+ timeoutPromise,
144
+ ]);
145
+ } catch (error) {
146
+ if (error instanceof Error && error.message.includes("timeout")) {
147
+ timedOut = true;
148
+ // Benchmark timed out
149
+ } else {
150
+ throw error;
151
+ }
152
+ } finally {
153
+ if (timeoutHandle) clearTimeout(timeoutHandle);
154
+ }
155
+
156
+ const totalDuration = Date.now() - benchmarkStart;
157
+ const avgIterationTime = totalDuration / completedIterations;
158
+
159
+ const result: BenchmarkResult = {
160
+ iterations: completedIterations,
161
+ totalDurationMs: totalDuration,
162
+ avgIterationMs: avgIterationTime,
163
+ timedOut,
164
+ };
165
+
166
+ // Benchmark results debug info removed to prevent console logging issues
167
+
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Helper function to run an operation with adaptive iterations and a callback.
173
+ * This is useful for tests that need to process results after each iteration.
174
+ *
175
+ * @param operation - The async function that returns a value
176
+ * @param callback - Function to process each result
177
+ * @param options - Configuration options for the benchmark
178
+ */
179
+ export async function runAdaptiveBenchmarkWithCallback<T>(
180
+ operation: () => Promise<T>,
181
+ callback: (result: T, iteration: number) => void | Promise<void>,
182
+ options: BenchmarkOptions = {},
183
+ ): Promise<BenchmarkResult> {
184
+ let iterationCount = 0;
185
+
186
+ const wrappedOperation = async () => {
187
+ const result = await operation();
188
+ await callback(result, iterationCount++);
189
+ };
190
+
191
+ return runAdaptiveBenchmark(wrappedOperation, options);
192
+ }
@@ -1,13 +1,41 @@
1
1
  import { debugLogContext, isDebugEnabled } from "../debuglog";
2
2
 
3
+ // Ensure clean process state on Windows
4
+ process.on("uncaughtException", (err) => {
5
+ const errorMessage = err instanceof Error ? err.message : String(err);
6
+ process.stderr.write(
7
+ `Uncaught exception in debuglog-child: ${errorMessage}\n`,
8
+ );
9
+ process.exit(1);
10
+ });
11
+
12
+ process.on("unhandledRejection", (reason) => {
13
+ const errorMessage =
14
+ reason instanceof Error ? reason.message : String(reason);
15
+ process.stderr.write(
16
+ `Unhandled rejection in debuglog-child: ${errorMessage}\n`,
17
+ );
18
+ process.exit(1);
19
+ });
20
+
3
21
  try {
4
22
  const result = {
5
23
  isDebugEnabled: isDebugEnabled(),
6
24
  debugLogContext: debugLogContext(),
7
25
  };
8
- console.log(JSON.stringify(result));
26
+ // Use process.stdout.write to ensure clean output
27
+ process.stdout.write(JSON.stringify(result));
9
28
  process.exit(0);
10
29
  } catch (err) {
11
- console.error(err);
30
+ // Don't log the error object directly as it might have circular references
31
+ // that cause issues with Jest's message passing on Windows
32
+ const errorMessage = err instanceof Error ? err.message : String(err);
33
+ process.stderr.write(`Error in debuglog-child: ${errorMessage}\n`);
34
+
35
+ // Also log stack trace for debugging
36
+ if (err instanceof Error && err.stack) {
37
+ process.stderr.write(`Stack trace:\n${err.stack}\n`);
38
+ }
39
+
12
40
  process.exit(1);
13
41
  }
@@ -1,10 +1,40 @@
1
1
  import { debug } from "../debuglog";
2
2
 
3
- // This will be run with NODE_DEBUG set, so debug should be enabled
4
- debug("test message %s %d", "hello", 42);
5
- debug("simple message");
6
- debug("object %o", { key: "value" });
7
-
8
- // Signal successful completion
9
- console.log("DONE");
10
- process.exit(0);
3
+ // Ensure clean process state on Windows
4
+ process.on("uncaughtException", (err) => {
5
+ const errorMessage = err instanceof Error ? err.message : String(err);
6
+ process.stderr.write(
7
+ `Uncaught exception in debuglog-enabled-child: ${errorMessage}\n`,
8
+ );
9
+ process.exit(1);
10
+ });
11
+
12
+ process.on("unhandledRejection", (reason) => {
13
+ const errorMessage =
14
+ reason instanceof Error ? reason.message : String(reason);
15
+ process.stderr.write(
16
+ `Unhandled rejection in debuglog-enabled-child: ${errorMessage}\n`,
17
+ );
18
+ process.exit(1);
19
+ });
20
+
21
+ try {
22
+ // This will be run with NODE_DEBUG set, so debug should be enabled
23
+ debug("test message %s %d", "hello", 42);
24
+ debug("simple message");
25
+ debug("object %o", { key: "value" });
26
+
27
+ // Signal successful completion using stdout.write for clean output
28
+ process.stdout.write("DONE");
29
+ process.exit(0);
30
+ } catch (err) {
31
+ const errorMessage = err instanceof Error ? err.message : String(err);
32
+ process.stderr.write(`Error in debuglog-enabled-child: ${errorMessage}\n`);
33
+
34
+ // Also log stack trace for debugging
35
+ if (err instanceof Error && err.stack) {
36
+ process.stderr.write(`Stack trace:\n${err.stack}\n`);
37
+ }
38
+
39
+ process.exit(1);
40
+ }
@@ -0,0 +1,14 @@
1
+ // Jest setup file - runs before all tests
2
+ // Configures global test environment and timeouts
3
+
4
+ import { jest } from "@jest/globals";
5
+ import { getTestTimeout } from "./test-timeout-config";
6
+
7
+ // Configure Jest timeout globally for all tests
8
+ jest.setTimeout(getTestTimeout());
9
+
10
+ // Set consistent timezone for tests (similar to PhotoStructure approach)
11
+ process.env["TZ"] = "America/Los_Angeles";
12
+
13
+ // Ensure test environment
14
+ process.env["NODE_ENV"] = "test";
@@ -0,0 +1,336 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { env, platform } from "node:process";
6
+ import { delay } from "../async";
7
+ import {
8
+ getAllVolumeMetadata,
9
+ getVolumeMetadata,
10
+ getVolumeMountPoints,
11
+ isHidden,
12
+ setHidden,
13
+ } from "../index";
14
+ import { randomLetters } from "../random";
15
+ import { MiB } from "../units";
16
+ import { runAdaptiveBenchmarkWithCallback } from "./benchmark-harness";
17
+ import { getTimingMultiplier } from "./test-timeout-config";
18
+
19
+ // Core memory testing logic for the standalone memory test runner
20
+ // This module contains all memory leak detection tests, designed to run
21
+ // outside of Jest for more accurate memory measurements and to avoid
22
+ // Jest worker process issues on Windows CI.
23
+
24
+ // Simple assertion helper
25
+ function assert(condition: boolean, message: string): void {
26
+ if (!condition) {
27
+ throw new Error(message);
28
+ }
29
+ }
30
+
31
+ // Enable garbage collection access
32
+ declare const global: {
33
+ gc?: () => void;
34
+ } & typeof globalThis;
35
+
36
+ // Helper function to get a temporary directory that's not hidden
37
+ function tmpDirNotHidden(): string {
38
+ const isMacOS = platform === "darwin";
39
+ const isWindows = platform === "win32";
40
+
41
+ const dir = isMacOS
42
+ ? join(homedir(), "tmp")
43
+ : isWindows
44
+ ? join(env["SystemDrive"] ?? "C:\\", "tmp")
45
+ : "/tmp";
46
+
47
+ mkdirSync(dir, { recursive: true });
48
+ return dir;
49
+ }
50
+
51
+ export interface MemoryTestResult {
52
+ testName: string;
53
+ passed: boolean;
54
+ initialMemory: number;
55
+ finalMemory: number;
56
+ memoryIncrease: number;
57
+ slope: number;
58
+ errorMessage?: string;
59
+ }
60
+
61
+ // Helper to get memory usage after GC
62
+ async function getMemoryUsage(): Promise<number> {
63
+ // Give things a bit to fall out of scope
64
+ // Use dynamic delay based on environment
65
+ const delayMs = Math.max(10, 100 * getTimingMultiplier());
66
+ await delay(delayMs);
67
+ if (global.gc) {
68
+ global.gc();
69
+ }
70
+ await delay(delayMs);
71
+ return process.memoryUsage().heapUsed;
72
+ }
73
+
74
+ /**
75
+ * Detects the likelihood of a memory leak based on memory usage values.
76
+ * @param memoryUsages - Array of memory usage values.
77
+ * @returns A boolean indicating if a memory leak is likely.
78
+ */
79
+ function leastSquaresSlope(memoryUsages: number[]): number {
80
+ const n = memoryUsages.length;
81
+ if (n < 2) return 0;
82
+
83
+ // Calculate the slope using linear regression (least squares method)
84
+ const xSum = (n * (n - 1)) / 2;
85
+ const ySum = memoryUsages.reduce((sum, usage) => sum + usage, 0);
86
+ const xySum = memoryUsages.reduce((sum, usage, i) => sum + i * usage, 0);
87
+ const xSquaredSum = (n * (n - 1) * (2 * n - 1)) / 6;
88
+
89
+ const result = (n * xySum - xSum * ySum) / (n * xSquaredSum - xSum * xSum);
90
+
91
+ // If the slope is positive and significant, it indicates a memory leak
92
+ return result;
93
+ }
94
+
95
+ // Helper to check if memory usage is stable
96
+ async function checkMemoryUsage(
97
+ operation: () => Promise<unknown>,
98
+ errorMarginBytes: number = 10 * MiB, // Alpine docker had a 5MB variance
99
+ maxAllowedSlope: number = 0.01,
100
+ ): Promise<{
101
+ passed: boolean;
102
+ initialMemory: number;
103
+ finalMemory: number;
104
+ slope: number;
105
+ errorMessage?: string;
106
+ }> {
107
+ // warm up memory consumption:
108
+ for (let i = 0; i < 5; i++) await operation();
109
+
110
+ // __then__ take a snapshot
111
+ const initialMemory = await getMemoryUsage();
112
+ const memoryUsages: number[] = [1];
113
+
114
+ // Run operations using adaptive benchmark
115
+ await runAdaptiveBenchmarkWithCallback(
116
+ operation,
117
+ async (_result, iteration) => {
118
+ // Take memory snapshots approximately 10 times during the benchmark
119
+ // Check every 10 iterations initially, will adjust based on actual runtime
120
+ if (iteration === 0 || iteration % 10 === 0) {
121
+ const currentMemory = await getMemoryUsage();
122
+ memoryUsages.push(currentMemory / initialMemory);
123
+ }
124
+ },
125
+ {
126
+ targetDurationMs: 10_000, // Reduced from 20s to 10s for faster CI
127
+ maxTimeoutMs: 30_000, // Reduced from 60s to 30s
128
+ minIterations: 10,
129
+ debug: !!process.env["DEBUG_BENCHMARK"],
130
+ },
131
+ );
132
+
133
+ // Final memory check
134
+ const finalMemory = await getMemoryUsage();
135
+ memoryUsages.push(finalMemory / initialMemory);
136
+ const slope = leastSquaresSlope(memoryUsages);
137
+
138
+ const memoryIncrease = finalMemory - initialMemory;
139
+ let errorMessage: string | undefined;
140
+
141
+ if (memoryIncrease >= errorMarginBytes) {
142
+ errorMessage = `Memory increased by ${(memoryIncrease / MiB).toFixed(2)} MiB, exceeding limit of ${(errorMarginBytes / MiB).toFixed(2)} MiB`;
143
+ } else if (slope >= maxAllowedSlope) {
144
+ errorMessage = `Memory slope ${slope.toFixed(4)} exceeds limit of ${maxAllowedSlope}`;
145
+ }
146
+
147
+ return {
148
+ passed: !errorMessage,
149
+ initialMemory,
150
+ finalMemory,
151
+ slope,
152
+ ...(errorMessage && { errorMessage }),
153
+ };
154
+ }
155
+
156
+ // Memory test implementations
157
+ export async function testVolumeMountPointsNoLeak(): Promise<MemoryTestResult> {
158
+ const result = await checkMemoryUsage(async () => {
159
+ const mountPoints = await getVolumeMountPoints();
160
+ if (mountPoints.length === 0) {
161
+ throw new Error("Expected at least one mount point");
162
+ }
163
+ });
164
+
165
+ return {
166
+ testName: "getVolumeMountPoints - no memory leak",
167
+ passed: result.passed,
168
+ initialMemory: result.initialMemory,
169
+ finalMemory: result.finalMemory,
170
+ memoryIncrease: result.finalMemory - result.initialMemory,
171
+ slope: result.slope,
172
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
173
+ };
174
+ }
175
+
176
+ export async function testVolumeMountPointsErrorConditions(): Promise<MemoryTestResult> {
177
+ const result = await checkMemoryUsage(async () => {
178
+ try {
179
+ await getVolumeMountPoints({ timeoutMs: 1 });
180
+ } catch {
181
+ // Expected
182
+ }
183
+ });
184
+
185
+ return {
186
+ testName: "getVolumeMountPoints - error conditions",
187
+ passed: result.passed,
188
+ initialMemory: result.initialMemory,
189
+ finalMemory: result.finalMemory,
190
+ memoryIncrease: result.finalMemory - result.initialMemory,
191
+ slope: result.slope,
192
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
193
+ };
194
+ }
195
+
196
+ export async function testGetAllVolumeMetadataNoLeak(): Promise<MemoryTestResult> {
197
+ const result = await checkMemoryUsage(async () => {
198
+ const metadata = await getAllVolumeMetadata();
199
+ if (metadata.length === 0) {
200
+ throw new Error("Expected at least one volume");
201
+ }
202
+ });
203
+
204
+ return {
205
+ testName: "getAllVolumeMetadata - no memory leak",
206
+ passed: result.passed,
207
+ initialMemory: result.initialMemory,
208
+ finalMemory: result.finalMemory,
209
+ memoryIncrease: result.finalMemory - result.initialMemory,
210
+ slope: result.slope,
211
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
212
+ };
213
+ }
214
+
215
+ export async function testGetVolumeMetadataErrorConditions(): Promise<MemoryTestResult> {
216
+ const result = await checkMemoryUsage(async () => {
217
+ try {
218
+ await getVolumeMetadata("nonexistent");
219
+ } catch {
220
+ // Expected
221
+ }
222
+ });
223
+
224
+ return {
225
+ testName: "getVolumeMetadata - error conditions",
226
+ passed: result.passed,
227
+ initialMemory: result.initialMemory,
228
+ finalMemory: result.finalMemory,
229
+ memoryIncrease: result.finalMemory - result.initialMemory,
230
+ slope: result.slope,
231
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
232
+ };
233
+ }
234
+
235
+ export async function testIsHiddenSetHiddenNoLeak(): Promise<MemoryTestResult> {
236
+ const testDir = await mkdtemp(join(tmpDirNotHidden(), "memory-tests-"));
237
+ let counter = 0;
238
+
239
+ try {
240
+ const result = await checkMemoryUsage(async () => {
241
+ // Create a unique subdirectory for each iteration to avoid path conflicts
242
+ const iterationDir = join(testDir, `iteration-${counter++}`);
243
+
244
+ // Simple validateHidden implementation without Jest dependencies
245
+ mkdirSync(iterationDir, { recursive: true });
246
+
247
+ // Test isHidden on a regular directory (should be false)
248
+ const isHiddenResult = await isHidden(iterationDir);
249
+ assert(
250
+ isHiddenResult === false,
251
+ `Expected ${iterationDir} to not be hidden`,
252
+ );
253
+
254
+ // Test setHidden to true
255
+ // On Linux, setHidden renames the file/dir by adding a dot prefix,
256
+ // so we need to capture the returned pathname
257
+ const hiddenPath = (await setHidden(iterationDir, true)).pathname;
258
+ const isHiddenAfterSet = await isHidden(hiddenPath);
259
+ assert(
260
+ isHiddenAfterSet === true,
261
+ `Expected ${hiddenPath} to be hidden after setHidden(true)`,
262
+ );
263
+
264
+ // Test setHidden to false
265
+ const visiblePath = (await setHidden(hiddenPath, false)).pathname;
266
+ const isHiddenAfterUnset = await isHidden(visiblePath);
267
+ assert(
268
+ isHiddenAfterUnset === false,
269
+ `Expected ${visiblePath} to not be hidden after setHidden(false)`,
270
+ );
271
+ });
272
+
273
+ return {
274
+ testName: "isHidden/setHidden - no memory leak",
275
+ passed: result.passed,
276
+ initialMemory: result.initialMemory,
277
+ finalMemory: result.finalMemory,
278
+ memoryIncrease: result.finalMemory - result.initialMemory,
279
+ slope: result.slope,
280
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
281
+ };
282
+ } finally {
283
+ await rm(testDir, { recursive: true, force: true }).catch(() => null);
284
+ }
285
+ }
286
+
287
+ export async function testIsHiddenSetHiddenErrorConditions(): Promise<MemoryTestResult> {
288
+ const testDir = await mkdtemp(join(tmpDirNotHidden(), "memory-tests-"));
289
+
290
+ try {
291
+ const result = await checkMemoryUsage(async () => {
292
+ const notafile = join(testDir, "nonexistent", "file-" + randomLetters(8));
293
+ try {
294
+ await isHidden(notafile);
295
+ } catch {
296
+ // Expected
297
+ }
298
+ try {
299
+ await setHidden(notafile, true);
300
+ } catch {
301
+ // Expected
302
+ }
303
+ });
304
+
305
+ return {
306
+ testName: "isHidden/setHidden - error conditions",
307
+ passed: result.passed,
308
+ initialMemory: result.initialMemory,
309
+ finalMemory: result.finalMemory,
310
+ memoryIncrease: result.finalMemory - result.initialMemory,
311
+ slope: result.slope,
312
+ ...(result.errorMessage && { errorMessage: result.errorMessage }),
313
+ };
314
+ } finally {
315
+ await rm(testDir, { recursive: true, force: true }).catch(() => null);
316
+ }
317
+ }
318
+
319
+ // Run all memory tests
320
+ export async function runAllMemoryTests(): Promise<MemoryTestResult[]> {
321
+ if (!global.gc) {
322
+ throw new Error("Garbage collection must be exposed. Run with --expose-gc");
323
+ }
324
+
325
+ const results: MemoryTestResult[] = [];
326
+
327
+ // Run each test and collect results
328
+ results.push(await testVolumeMountPointsNoLeak());
329
+ results.push(await testVolumeMountPointsErrorConditions());
330
+ results.push(await testGetAllVolumeMetadataNoLeak());
331
+ results.push(await testGetVolumeMetadataErrorConditions());
332
+ results.push(await testIsHiddenSetHiddenNoLeak());
333
+ results.push(await testIsHiddenSetHiddenErrorConditions());
334
+
335
+ return results;
336
+ }