master
go 661 lines 24 KB
Raw
1 package cli
2
3 import (
4 "io"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/ipfs/kubo/config"
12 "github.com/ipfs/kubo/test/cli/harness"
13 "github.com/ipfs/kubo/test/cli/testutils"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 )
17
18 // waitForLogMessage polls a buffer for a log message, waiting up to timeout duration.
19 // Returns true if message found, false if timeout reached.
20 func waitForLogMessage(buffer *harness.Buffer, message string, timeout time.Duration) bool {
21 deadline := time.Now().Add(timeout)
22 for time.Now().Before(deadline) {
23 if strings.Contains(buffer.String(), message) {
24 return true
25 }
26 time.Sleep(100 * time.Millisecond)
27 }
28 return false
29 }
30
31 func TestAdd(t *testing.T) {
32 t.Parallel()
33
34 var (
35 shortString = "hello world"
36 shortStringCidV0 = "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD" // cidv0 - dag-pb - sha2-256
37 shortStringCidV1 = "bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e" // cidv1 - raw - sha2-256
38 shortStringCidV1NoRawLeaves = "bafybeihykld7uyxzogax6vgyvag42y7464eywpf55gxi5qpoisibh3c5wa" // cidv1 - dag-pb - sha2-256
39 shortStringCidV1Sha512 = "bafkrgqbqt3gerhas23vuzrapkdeqf4vu2dwxp3srdj6hvg6nhsug2tgyn6mj3u23yx7utftq3i2ckw2fwdh5qmhid5qf3t35yvkc5e5ottlw6"
40 )
41
42 t.Run("produced cid version: implicit default (CIDv0)", func(t *testing.T) {
43 t.Parallel()
44 node := harness.NewT(t).NewNode().Init().StartDaemon()
45 defer node.StopDaemon()
46
47 cidStr := node.IPFSAddStr(shortString)
48 require.Equal(t, shortStringCidV0, cidStr)
49 })
50
51 t.Run("produced cid version: follows user-set configuration Import.CidVersion=0", func(t *testing.T) {
52 t.Parallel()
53 node := harness.NewT(t).NewNode().Init()
54 node.UpdateConfig(func(cfg *config.Config) {
55 cfg.Import.CidVersion = *config.NewOptionalInteger(0)
56 })
57 node.StartDaemon()
58 defer node.StopDaemon()
59
60 cidStr := node.IPFSAddStr(shortString)
61 require.Equal(t, shortStringCidV0, cidStr)
62 })
63
64 t.Run("produced cid multihash: follows user-set configuration in Import.HashFunction", func(t *testing.T) {
65 t.Parallel()
66 node := harness.NewT(t).NewNode().Init()
67 node.UpdateConfig(func(cfg *config.Config) {
68 cfg.Import.HashFunction = *config.NewOptionalString("sha2-512")
69 })
70 node.StartDaemon()
71 defer node.StopDaemon()
72
73 cidStr := node.IPFSAddStr(shortString)
74 require.Equal(t, shortStringCidV1Sha512, cidStr)
75 })
76
77 t.Run("produced cid version: follows user-set configuration Import.CidVersion=1", func(t *testing.T) {
78 t.Parallel()
79 node := harness.NewT(t).NewNode().Init()
80 node.UpdateConfig(func(cfg *config.Config) {
81 cfg.Import.CidVersion = *config.NewOptionalInteger(1)
82 })
83 node.StartDaemon()
84 defer node.StopDaemon()
85
86 cidStr := node.IPFSAddStr(shortString)
87 require.Equal(t, shortStringCidV1, cidStr)
88 })
89
90 t.Run("produced cid version: command flag overrides configuration in Import.CidVersion", func(t *testing.T) {
91 t.Parallel()
92 node := harness.NewT(t).NewNode().Init()
93 node.UpdateConfig(func(cfg *config.Config) {
94 cfg.Import.CidVersion = *config.NewOptionalInteger(1)
95 })
96 node.StartDaemon()
97 defer node.StopDaemon()
98
99 cidStr := node.IPFSAddStr(shortString, "--cid-version", "0")
100 require.Equal(t, shortStringCidV0, cidStr)
101 })
102
103 t.Run("produced unixfs raw leaves: follows user-set configuration Import.UnixFSRawLeaves", func(t *testing.T) {
104 t.Parallel()
105 node := harness.NewT(t).NewNode().Init()
106 node.UpdateConfig(func(cfg *config.Config) {
107 // CIDv1 defaults to raw-leaves=true
108 cfg.Import.CidVersion = *config.NewOptionalInteger(1)
109 // disable manually
110 cfg.Import.UnixFSRawLeaves = config.False
111 })
112 node.StartDaemon()
113 defer node.StopDaemon()
114
115 cidStr := node.IPFSAddStr(shortString)
116 require.Equal(t, shortStringCidV1NoRawLeaves, cidStr)
117 })
118
119 t.Run("ipfs add --pin-name=foo", func(t *testing.T) {
120 t.Parallel()
121 node := harness.NewT(t).NewNode().Init().StartDaemon()
122 defer node.StopDaemon()
123
124 pinName := "test-pin-name"
125 cidStr := node.IPFSAddStr(shortString, "--pin-name", pinName)
126 require.Equal(t, shortStringCidV0, cidStr)
127
128 pinList := node.IPFS("pin", "ls", "--names").Stdout.Trimmed()
129 require.Contains(t, pinList, shortStringCidV0)
130 require.Contains(t, pinList, pinName)
131 })
132
133 t.Run("ipfs add --pin=false --pin-name=foo returns an error", func(t *testing.T) {
134 t.Parallel()
135
136 node := harness.NewT(t).NewNode().Init().StartDaemon()
137 defer node.StopDaemon()
138
139 // Use RunIPFS to allow for errors without assertion
140 result := node.RunIPFS("add", "--pin=false", "--pin-name=foo")
141 require.Error(t, result.Err, "Expected an error due to incompatible --pin and --pin-name")
142 require.Contains(t, result.Stderr.String(), "pin-name option requires pin to be set")
143 })
144
145 t.Run("ipfs add --pin-name without value should fail", func(t *testing.T) {
146 t.Parallel()
147
148 node := harness.NewT(t).NewNode().Init().StartDaemon()
149 defer node.StopDaemon()
150
151 // When --pin-name is passed without any value, it should fail
152 result := node.RunIPFS("add", "--pin-name")
153 require.Error(t, result.Err, "Expected an error when --pin-name has no value")
154 require.Contains(t, result.Stderr.String(), "missing argument for option \"pin-name\"")
155 })
156
157 t.Run("produced unixfs max file links: command flag --max-file-links overrides configuration in Import.UnixFSFileMaxLinks", func(t *testing.T) {
158 t.Parallel()
159
160 //
161 // UnixFSChunker=size-262144 (256KiB)
162 // Import.UnixFSFileMaxLinks=174
163 node := harness.NewT(t).NewNode().Init("--profile=unixfs-v0-2015") // unixfs-v0-2015 for determinism across all params
164 node.UpdateConfig(func(cfg *config.Config) {
165 cfg.Import.UnixFSChunker = *config.NewOptionalString("size-262144") // 256 KiB chunks
166 cfg.Import.UnixFSFileMaxLinks = *config.NewOptionalInteger(174) // max 174 per level
167 })
168 node.StartDaemon()
169 defer node.StopDaemon()
170
171 // Add 174MiB file:
172 // 1024 * 256KiB should fit in single layer
173 seed := shortString
174 cidStr := node.IPFSAddDeterministic("262144KiB", seed, "--max-file-links", "1024")
175 root, err := node.InspectPBNode(cidStr)
176 assert.NoError(t, err)
177
178 // Expect 1024 links due to cli parameter raising link limit from 174 to 1024
179 require.Equal(t, 1024, len(root.Links))
180 // expect same CID every time
181 require.Equal(t, "QmbBftNHWmjSWKLC49dMVrfnY8pjrJYntiAXirFJ7oJrNk", cidStr)
182 })
183
184 // Profile-specific threshold tests are in cid_profiles_test.go (TestCIDProfiles).
185 // Tests here cover general ipfs add behavior not tied to specific profiles.
186
187 t.Run("ipfs add --hidden", func(t *testing.T) {
188 t.Parallel()
189
190 // Helper to create test directory with hidden file
191 setupTestDir := func(t *testing.T, node *harness.Node) string {
192 testDir, err := os.MkdirTemp(node.Dir, "hidden-test")
193 require.NoError(t, err)
194 require.NoError(t, os.WriteFile(filepath.Join(testDir, "visible.txt"), []byte("visible"), 0o644))
195 require.NoError(t, os.WriteFile(filepath.Join(testDir, ".hidden"), []byte("hidden"), 0o644))
196 return testDir
197 }
198
199 t.Run("default excludes hidden files", func(t *testing.T) {
200 t.Parallel()
201 node := harness.NewT(t).NewNode().Init().StartDaemon()
202 defer node.StopDaemon()
203
204 testDir := setupTestDir(t, node)
205 cidStr := node.IPFS("add", "-r", "-Q", testDir).Stdout.Trimmed()
206 lsOutput := node.IPFS("ls", cidStr).Stdout.Trimmed()
207 require.Contains(t, lsOutput, "visible.txt")
208 require.NotContains(t, lsOutput, ".hidden")
209 })
210
211 t.Run("--hidden includes hidden files", func(t *testing.T) {
212 t.Parallel()
213 node := harness.NewT(t).NewNode().Init().StartDaemon()
214 defer node.StopDaemon()
215
216 testDir := setupTestDir(t, node)
217 cidStr := node.IPFS("add", "-r", "-Q", "--hidden", testDir).Stdout.Trimmed()
218 lsOutput := node.IPFS("ls", cidStr).Stdout.Trimmed()
219 require.Contains(t, lsOutput, "visible.txt")
220 require.Contains(t, lsOutput, ".hidden")
221 })
222
223 t.Run("-H includes hidden files", func(t *testing.T) {
224 t.Parallel()
225 node := harness.NewT(t).NewNode().Init().StartDaemon()
226 defer node.StopDaemon()
227
228 testDir := setupTestDir(t, node)
229 cidStr := node.IPFS("add", "-r", "-Q", "-H", testDir).Stdout.Trimmed()
230 lsOutput := node.IPFS("ls", cidStr).Stdout.Trimmed()
231 require.Contains(t, lsOutput, "visible.txt")
232 require.Contains(t, lsOutput, ".hidden")
233 })
234 })
235
236 t.Run("ipfs add --empty-dirs", func(t *testing.T) {
237 t.Parallel()
238
239 // Helper to create test directory with empty subdirectory
240 setupTestDir := func(t *testing.T, node *harness.Node) string {
241 testDir, err := os.MkdirTemp(node.Dir, "empty-dirs-test")
242 require.NoError(t, err)
243 require.NoError(t, os.Mkdir(filepath.Join(testDir, "empty-subdir"), 0o755))
244 require.NoError(t, os.WriteFile(filepath.Join(testDir, "file.txt"), []byte("content"), 0o644))
245 return testDir
246 }
247
248 t.Run("default includes empty directories", func(t *testing.T) {
249 t.Parallel()
250 node := harness.NewT(t).NewNode().Init().StartDaemon()
251 defer node.StopDaemon()
252
253 testDir := setupTestDir(t, node)
254 cidStr := node.IPFS("add", "-r", "-Q", testDir).Stdout.Trimmed()
255 require.Contains(t, node.IPFS("ls", cidStr).Stdout.Trimmed(), "empty-subdir")
256 })
257
258 t.Run("--empty-dirs=true includes empty directories", func(t *testing.T) {
259 t.Parallel()
260 node := harness.NewT(t).NewNode().Init().StartDaemon()
261 defer node.StopDaemon()
262
263 testDir := setupTestDir(t, node)
264 cidStr := node.IPFS("add", "-r", "-Q", "--empty-dirs=true", testDir).Stdout.Trimmed()
265 require.Contains(t, node.IPFS("ls", cidStr).Stdout.Trimmed(), "empty-subdir")
266 })
267
268 t.Run("--empty-dirs=false excludes empty directories", func(t *testing.T) {
269 t.Parallel()
270 node := harness.NewT(t).NewNode().Init().StartDaemon()
271 defer node.StopDaemon()
272
273 testDir := setupTestDir(t, node)
274 cidStr := node.IPFS("add", "-r", "-Q", "--empty-dirs=false", testDir).Stdout.Trimmed()
275 lsOutput := node.IPFS("ls", cidStr).Stdout.Trimmed()
276 require.NotContains(t, lsOutput, "empty-subdir")
277 require.Contains(t, lsOutput, "file.txt")
278 })
279 })
280
281 t.Run("ipfs add symlink handling", func(t *testing.T) {
282 t.Parallel()
283
284 // Helper to create test directory structure:
285 // testDir/
286 // target.txt (file with "target content")
287 // link.txt -> target.txt (symlink at top level)
288 // subdir/
289 // subsubdir/
290 // nested-target.txt (file with "nested content")
291 // nested-link.txt -> nested-target.txt (symlink in sub-sub directory)
292 setupTestDir := func(t *testing.T, node *harness.Node) string {
293 testDir, err := os.MkdirTemp(node.Dir, "deref-symlinks-test")
294 require.NoError(t, err)
295
296 // Top-level file and symlink
297 targetFile := filepath.Join(testDir, "target.txt")
298 require.NoError(t, os.WriteFile(targetFile, []byte("target content"), 0o644))
299 require.NoError(t, os.Symlink("target.txt", filepath.Join(testDir, "link.txt")))
300
301 // Nested file and symlink in sub-sub directory
302 subsubdir := filepath.Join(testDir, "subdir", "subsubdir")
303 require.NoError(t, os.MkdirAll(subsubdir, 0o755))
304 nestedTarget := filepath.Join(subsubdir, "nested-target.txt")
305 require.NoError(t, os.WriteFile(nestedTarget, []byte("nested content"), 0o644))
306 require.NoError(t, os.Symlink("nested-target.txt", filepath.Join(subsubdir, "nested-link.txt")))
307
308 return testDir
309 }
310
311 t.Run("default preserves symlinks", func(t *testing.T) {
312 t.Parallel()
313 node := harness.NewT(t).NewNode().Init().StartDaemon()
314 defer node.StopDaemon()
315
316 testDir := setupTestDir(t, node)
317
318 // Add directory with symlink (default: preserve)
319 dirCID := node.IPFS("add", "-r", "-Q", testDir).Stdout.Trimmed()
320
321 // Get and verify symlinks are preserved
322 outDir, err := os.MkdirTemp(node.Dir, "symlink-get-out")
323 require.NoError(t, err)
324 node.IPFS("get", "-o", outDir, dirCID)
325
326 // Check top-level symlink is preserved
327 linkPath := filepath.Join(outDir, "link.txt")
328 fi, err := os.Lstat(linkPath)
329 require.NoError(t, err)
330 require.True(t, fi.Mode()&os.ModeSymlink != 0, "link.txt should be a symlink")
331 target, err := os.Readlink(linkPath)
332 require.NoError(t, err)
333 require.Equal(t, "target.txt", target)
334
335 // Check nested symlink is preserved
336 nestedLinkPath := filepath.Join(outDir, "subdir", "subsubdir", "nested-link.txt")
337 fi, err = os.Lstat(nestedLinkPath)
338 require.NoError(t, err)
339 require.True(t, fi.Mode()&os.ModeSymlink != 0, "nested-link.txt should be a symlink")
340 })
341
342 // --dereference-args is deprecated but still works for backwards compatibility.
343 // It only resolves symlinks passed as CLI arguments, NOT symlinks found
344 // during directory traversal. Use --dereference-symlinks instead.
345 t.Run("--dereference-args resolves CLI args only", func(t *testing.T) {
346 t.Parallel()
347 node := harness.NewT(t).NewNode().Init().StartDaemon()
348 defer node.StopDaemon()
349
350 testDir := setupTestDir(t, node)
351 symlinkPath := filepath.Join(testDir, "link.txt")
352 targetPath := filepath.Join(testDir, "target.txt")
353
354 symlinkCID := node.IPFS("add", "-Q", "--dereference-args", symlinkPath).Stdout.Trimmed()
355 targetCID := node.IPFS("add", "-Q", targetPath).Stdout.Trimmed()
356
357 // CIDs should match because --dereference-args resolves the symlink
358 require.Equal(t, targetCID, symlinkCID,
359 "--dereference-args should resolve CLI arg symlink to target content")
360
361 // Now add the directory recursively with --dereference-args
362 // Nested symlinks should NOT be resolved (only CLI args are resolved)
363 dirCID := node.IPFS("add", "-r", "-Q", "--dereference-args", testDir).Stdout.Trimmed()
364
365 outDir, err := os.MkdirTemp(node.Dir, "deref-args-out")
366 require.NoError(t, err)
367 node.IPFS("get", "-o", outDir, dirCID)
368
369 // Nested symlink should still be a symlink (not dereferenced)
370 nestedLinkPath := filepath.Join(outDir, "subdir", "subsubdir", "nested-link.txt")
371 fi, err := os.Lstat(nestedLinkPath)
372 require.NoError(t, err)
373 require.True(t, fi.Mode()&os.ModeSymlink != 0,
374 "--dereference-args should NOT resolve nested symlinks, only CLI args")
375 })
376
377 // --dereference-symlinks resolves ALL symlinks: both CLI arguments AND
378 // symlinks found during directory traversal. This is a superset of
379 // the deprecated --dereference-args behavior.
380 t.Run("--dereference-symlinks resolves all symlinks", func(t *testing.T) {
381 t.Parallel()
382 node := harness.NewT(t).NewNode().Init().StartDaemon()
383 defer node.StopDaemon()
384
385 testDir := setupTestDir(t, node)
386 symlinkPath := filepath.Join(testDir, "link.txt")
387 targetPath := filepath.Join(testDir, "target.txt")
388
389 symlinkCID := node.IPFS("add", "-Q", "--dereference-symlinks", symlinkPath).Stdout.Trimmed()
390 targetCID := node.IPFS("add", "-Q", targetPath).Stdout.Trimmed()
391
392 require.Equal(t, targetCID, symlinkCID,
393 "--dereference-symlinks should resolve CLI arg symlink (like --dereference-args)")
394
395 // Test 2: Nested symlinks in sub-sub directory are ALSO resolved
396 dirCID := node.IPFS("add", "-r", "-Q", "--dereference-symlinks", testDir).Stdout.Trimmed()
397
398 outDir, err := os.MkdirTemp(node.Dir, "deref-symlinks-out")
399 require.NoError(t, err)
400 node.IPFS("get", "-o", outDir, dirCID)
401
402 // Top-level symlink should be dereferenced to regular file
403 linkPath := filepath.Join(outDir, "link.txt")
404 fi, err := os.Lstat(linkPath)
405 require.NoError(t, err)
406 require.False(t, fi.Mode()&os.ModeSymlink != 0,
407 "link.txt should be dereferenced to regular file")
408 content, err := os.ReadFile(linkPath)
409 require.NoError(t, err)
410 require.Equal(t, "target content", string(content))
411
412 // Nested symlink in sub-sub directory should ALSO be dereferenced
413 nestedLinkPath := filepath.Join(outDir, "subdir", "subsubdir", "nested-link.txt")
414 fi, err = os.Lstat(nestedLinkPath)
415 require.NoError(t, err)
416 require.False(t, fi.Mode()&os.ModeSymlink != 0,
417 "nested-link.txt should be dereferenced (--dereference-symlinks resolves ALL symlinks)")
418 nestedContent, err := os.ReadFile(nestedLinkPath)
419 require.NoError(t, err)
420 require.Equal(t, "nested content", string(nestedContent))
421 })
422 })
423 }
424
425 func TestAddFastProvide(t *testing.T) {
426 t.Parallel()
427
428 const (
429 shortString = "hello world"
430 shortStringCidV0 = "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD" // cidv0 - dag-pb - sha2-256
431 )
432
433 t.Run("fast-provide-root disabled via config: verify skipped in logs", func(t *testing.T) {
434 t.Parallel()
435 node := harness.NewT(t).NewNode().Init()
436 node.UpdateConfig(func(cfg *config.Config) {
437 cfg.Import.FastProvideRoot = config.False
438 })
439
440 // Start daemon with debug logging
441 node.StartDaemonWithReq(harness.RunRequest{
442 CmdOpts: []harness.CmdOpt{
443 harness.RunWithEnv(map[string]string{
444 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
445 }),
446 },
447 }, "")
448 defer node.StopDaemon()
449
450 cidStr := node.IPFSAddStr(shortString)
451 require.Equal(t, shortStringCidV0, cidStr)
452
453 // Verify fast-provide-root was disabled
454 daemonLog := node.Daemon.Stderr.String()
455 require.Contains(t, daemonLog, "fast-provide-root: skipped")
456 })
457
458 t.Run("fast-provide-root enabled with wait=false: verify async provide", func(t *testing.T) {
459 t.Parallel()
460 node := harness.NewT(t).NewNode().Init()
461 // Use default config (FastProvideRoot=true, FastProvideWait=false)
462
463 node.StartDaemonWithReq(harness.RunRequest{
464 CmdOpts: []harness.CmdOpt{
465 harness.RunWithEnv(map[string]string{
466 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
467 }),
468 },
469 }, "")
470 defer node.StopDaemon()
471
472 cidStr := node.IPFSAddStr(shortString)
473 require.Equal(t, shortStringCidV0, cidStr)
474
475 daemonLog := node.Daemon.Stderr
476 // Should see async mode started
477 require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
478 require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
479
480 // Wait for async completion or failure (up to 11 seconds - slightly more than fastProvideTimeout)
481 // In test environment with no DHT peers, this will fail with "failed to find any peer in table"
482 completedOrFailed := waitForLogMessage(daemonLog, "async provide completed", 11*time.Second) ||
483 waitForLogMessage(daemonLog, "async provide failed", 11*time.Second)
484 require.True(t, completedOrFailed, "async provide should complete or fail within timeout")
485 })
486
487 t.Run("fast-provide-root enabled with wait=true: verify sync provide", func(t *testing.T) {
488 t.Parallel()
489 node := harness.NewT(t).NewNode().Init()
490 node.UpdateConfig(func(cfg *config.Config) {
491 cfg.Import.FastProvideWait = config.True
492 })
493
494 node.StartDaemonWithReq(harness.RunRequest{
495 CmdOpts: []harness.CmdOpt{
496 harness.RunWithEnv(map[string]string{
497 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
498 }),
499 },
500 }, "")
501 defer node.StopDaemon()
502
503 // Use Runner.Run with stdin to allow for expected errors
504 res := node.Runner.Run(harness.RunRequest{
505 Path: node.IPFSBin,
506 Args: []string{"add", "-q"},
507 CmdOpts: []harness.CmdOpt{
508 harness.RunWithStdin(strings.NewReader(shortString)),
509 },
510 })
511
512 // In sync mode (wait=true), provide errors propagate and fail the command.
513 // Test environment uses 'test' profile with no bootstrappers, and CI has
514 // insufficient peers for proper DHT puts, so we expect this to fail with
515 // "failed to find any peer in table" error from the DHT.
516 require.Equal(t, 1, res.ExitCode())
517 require.Contains(t, res.Stderr.String(), "Error: fast-provide: failed to find any peer in table")
518
519 daemonLog := node.Daemon.Stderr.String()
520 // Should see sync mode started
521 require.Contains(t, daemonLog, "fast-provide-root: enabled")
522 require.Contains(t, daemonLog, "fast-provide-root: providing synchronously")
523 require.Contains(t, daemonLog, "sync provide failed") // Verify the failure was logged
524 })
525
526 t.Run("fast-provide-wait ignored when root disabled", func(t *testing.T) {
527 t.Parallel()
528 node := harness.NewT(t).NewNode().Init()
529 node.UpdateConfig(func(cfg *config.Config) {
530 cfg.Import.FastProvideRoot = config.False
531 cfg.Import.FastProvideWait = config.True
532 })
533
534 node.StartDaemonWithReq(harness.RunRequest{
535 CmdOpts: []harness.CmdOpt{
536 harness.RunWithEnv(map[string]string{
537 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
538 }),
539 },
540 }, "")
541 defer node.StopDaemon()
542
543 cidStr := node.IPFSAddStr(shortString)
544 require.Equal(t, shortStringCidV0, cidStr)
545
546 daemonLog := node.Daemon.Stderr.String()
547 require.Contains(t, daemonLog, "fast-provide-root: skipped")
548 require.Contains(t, daemonLog, "wait-flag-ignored")
549 })
550
551 t.Run("CLI flag overrides config: flag=true overrides config=false", func(t *testing.T) {
552 t.Parallel()
553 node := harness.NewT(t).NewNode().Init()
554 node.UpdateConfig(func(cfg *config.Config) {
555 cfg.Import.FastProvideRoot = config.False
556 })
557
558 node.StartDaemonWithReq(harness.RunRequest{
559 CmdOpts: []harness.CmdOpt{
560 harness.RunWithEnv(map[string]string{
561 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
562 }),
563 },
564 }, "")
565 defer node.StopDaemon()
566
567 cidStr := node.IPFSAddStr(shortString, "--fast-provide-root=true")
568 require.Equal(t, shortStringCidV0, cidStr)
569
570 daemonLog := node.Daemon.Stderr
571 // Flag should enable it despite config saying false
572 require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
573 require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
574 })
575
576 t.Run("CLI flag overrides config: flag=false overrides config=true", func(t *testing.T) {
577 t.Parallel()
578 node := harness.NewT(t).NewNode().Init()
579 node.UpdateConfig(func(cfg *config.Config) {
580 cfg.Import.FastProvideRoot = config.True
581 })
582
583 node.StartDaemonWithReq(harness.RunRequest{
584 CmdOpts: []harness.CmdOpt{
585 harness.RunWithEnv(map[string]string{
586 "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
587 }),
588 },
589 }, "")
590 defer node.StopDaemon()
591
592 cidStr := node.IPFSAddStr(shortString, "--fast-provide-root=false")
593 require.Equal(t, shortStringCidV0, cidStr)
594
595 daemonLog := node.Daemon.Stderr.String()
596 // Flag should disable it despite config saying true
597 require.Contains(t, daemonLog, "fast-provide-root: skipped")
598 })
599 }
600
601 // createDirectoryForHAMTLinksEstimation creates a directory with the specified number
602 // of files for testing links-based size estimation (size = sum of nameLen + cidLen).
603 // Used by legacy profiles (unixfs-v0-2015).
604 //
605 // The lastNameLen parameter allows the last file to have a different name length,
606 // enabling exact +1 byte threshold tests.
607 func createDirectoryForHAMTLinksEstimation(dirPath string, numFiles, nameLen, lastNameLen int, seed string) error {
608 return createDeterministicFiles(dirPath, numFiles, nameLen, lastNameLen, seed)
609 }
610
611 // createDirectoryForHAMTBlockEstimation creates a directory with the specified number
612 // of files for testing block-based size estimation (LinkSerializedSize with protobuf overhead).
613 // Used by modern profiles (unixfs-v1-2025).
614 //
615 // The lastNameLen parameter allows the last file to have a different name length,
616 // enabling exact +1 byte threshold tests.
617 func createDirectoryForHAMTBlockEstimation(dirPath string, numFiles, nameLen, lastNameLen int, seed string) error {
618 return createDeterministicFiles(dirPath, numFiles, nameLen, lastNameLen, seed)
619 }
620
621 // createDeterministicFiles creates numFiles files with deterministic names.
622 // Files 0 to numFiles-2 have nameLen characters, and the last file has lastNameLen characters.
623 // Each file contains "x" (1 byte) for non-zero tsize in directory links.
624 func createDeterministicFiles(dirPath string, numFiles, nameLen, lastNameLen int, seed string) error {
625 alphabetLen := len(testutils.AlphabetEasy)
626
627 // Deterministic pseudo-random bytes for static filenames
628 drand, err := testutils.DeterministicRandomReader("1MiB", seed)
629 if err != nil {
630 return err
631 }
632
633 for i := range numFiles {
634 // Use lastNameLen for the final file
635 currentNameLen := nameLen
636 if i == numFiles-1 {
637 currentNameLen = lastNameLen
638 }
639
640 buf := make([]byte, currentNameLen)
641 _, err := io.ReadFull(drand, buf)
642 if err != nil {
643 return err
644 }
645
646 // Convert deterministic pseudo-random bytes to ASCII
647 var sb strings.Builder
648 for _, b := range buf {
649 char := testutils.AlphabetEasy[int(b)%alphabetLen]
650 sb.WriteRune(char)
651 }
652 filename := sb.String()[:currentNameLen]
653 filePath := filepath.Join(dirPath, filename)
654
655 // Create file with 1-byte content for non-zero tsize
656 if err := os.WriteFile(filePath, []byte("x"), 0o644); err != nil {
657 return err
658 }
659 }
660 return nil
661 }