agpm-cli 0.4.14

AGent Package Manager - A Git-based package manager for coding agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
// Scale tests for parallel processing in transitive dependency resolution
//
// Tests performance and scalability features:
// - Deep transitive chains (A→B→C→D)
// - 100+ parallel dependencies
// - Deterministic concurrent resolution
// - Performance under load
//
//! Performance thresholds (established on M1 MacBook Pro 2021, macOS 14):
//! - Deep chains (12 files): ~5-10s typical
//! - Scale test (110 files): ~15-30s typical
//! - Throughput: ~10-20 files/sec typical
//!
//! Tests log performance metrics but don't assert on timing to avoid CI flakes.
//! Monitor logs for performance regressions. Significant slowdowns (>3x baseline)
//! may indicate performance issues.

use anyhow::Result;
use std::time::Instant;

use crate::common::{ManifestBuilder, TestProject};

/// Test deep transitive chains under concurrent load (A→B→C→D)
#[tokio::test]
async fn test_deep_transitive_chains_concurrent() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;

    // Create source repo with deep dependency chains
    let chain_repo = project.create_source_repo("chain").await?;

    // Create a 4-level deep chain: A → B → C → D
    // Level D (leaf) - no dependencies
    chain_repo
        .add_resource(
            "agents",
            "level-d",
            r#"---
name: Level D Agent
# No dependencies - this is the leaf
---
# Level D Agent

This is the final level in the dependency chain with no further dependencies.
"#,
        )
        .await?;

    // Level C - depends on D
    chain_repo
        .add_resource(
            "agents",
            "level-c",
            r#"---
name: Level C Agent
dependencies:
  agents:
    - path: ./level-d.md
      version: v1.0.0
---
# Level C Agent

This agent depends on Level D.
"#,
        )
        .await?;

    // Level B - depends on C
    chain_repo
        .add_resource(
            "agents",
            "level-b",
            r#"---
name: Level B Agent
dependencies:
  agents:
    - path: ./level-c.md
      version: v1.0.0
---
# Level B Agent

This agent depends on Level C.
"#,
        )
        .await?;

    // Level A - depends on B
    chain_repo
        .add_resource(
            "agents",
            "level-a",
            r#"---
name: Level A Agent
dependencies:
  agents:
    - path: ./level-b.md
      version: v1.0.0
---
# Level A Agent

This agent depends on Level B.
"#,
        )
        .await?;

    // Create multiple parallel chains that converge on shared dependencies
    for i in 0..8 {
        chain_repo
            .add_resource(
                "agents",
                &format!("chain-{:02}-a", i),
                format!(
                    r#"---
dependencies:
  agents:
    - path: ./level-a.md
      version: v1.0.0
---
# Chain {:02} Level A

This chain starts at Level A.
"#,
                    i
                )
                .as_str(),
            )
            .await?;
    }

    chain_repo.commit_all("Add deep chain dependencies")?;
    chain_repo.tag_version("v1.0.0")?;

    // Create manifest with multiple parallel chains
    let source_url = chain_repo.bare_file_url(project.sources_path()).await?;
    let mut builder = ManifestBuilder::new().add_source("chain", &source_url);

    // Add all chain starting points
    for i in 0..8 {
        builder = builder.add_agent(&format!("chain-{:02}", i), |d| {
            d.source("chain").path(&format!("agents/chain-{:02}-a.md", i)).version("v1.0.0")
        });
    }

    let manifest = builder.build();
    project.write_manifest(&manifest).await?;

    // Measure time for performance verification
    let start_time = Instant::now();

    // Run install
    let output = project.run_agpm(&["install"])?;
    assert!(output.success, "Install should succeed. Stderr: {}", output.stderr);

    let install_duration = start_time.elapsed();

    // Verify all files were installed
    let agents_dir = project.project_path().join(".claude/agents/agpm");
    let mut installed_files = Vec::new();
    let mut entries = tokio::fs::read_dir(&agents_dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        installed_files.push(entry.file_name().to_string_lossy().to_string());
    }

    installed_files.sort();

    // Should have exactly 8 unique chain starting points + 4 intermediate levels = 12 files
    assert_eq!(
        installed_files.len(),
        12,
        "Expected exactly 12 installed files (8 chains + 4 levels), got {}. Files: {:?}",
        installed_files.len(),
        installed_files
    );

    // Verify specific files exist
    assert!(installed_files.contains(&"level-a.md".to_string()), "Level A should be installed");
    assert!(installed_files.contains(&"level-b.md".to_string()), "Level B should be installed");
    assert!(installed_files.contains(&"level-c.md".to_string()), "Level C should be installed");
    assert!(installed_files.contains(&"level-d.md".to_string()), "Level D should be installed");

    // Check for chain starting points
    for i in 0..8 {
        let expected_name = format!("chain-{:02}-a.md", i);
        assert!(
            installed_files.contains(&expected_name),
            "Chain starting point {} should be installed",
            expected_name
        );
    }

    // Performance check - should be reasonable for concurrent processing
    println!(
        "✅ Deep chain concurrent install completed in {:?} for {} files",
        install_duration,
        installed_files.len()
    );

    // Log performance metrics for monitoring
    println!(
        "Performance: Deep chain of {} files installed in {:?}",
        installed_files.len(),
        install_duration
    );

    // Very generous warning threshold (5x the original 30s limit)
    if install_duration.as_secs() > 150 {
        eprintln!(
            "⚠️  Warning: Deep chain install took unusually long ({:?}), may indicate performance issue",
            install_duration
        );
    }

    Ok(())
}

/// Test stress scenario with 100+ parallel transitive dependencies
#[tokio::test]
async fn test_stress_100_parallel_dependencies() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    let project = TestProject::new().await?;

    // Create source repo with many dependencies
    let stress_repo = project.create_source_repo("stress").await?;

    // Add base dependencies (20)
    for i in 0..20 {
        stress_repo
            .add_resource(
                "agents",
                &format!("base-{:03}", i),
                r#"---
# Base dependency agent
This is a base dependency with no further dependencies.
---
"#,
            )
            .await?;
    }

    // Add intermediate dependencies that depend on base ones (40)
    for i in 0..40 {
        let base_dep = i % 20;
        stress_repo
            .add_resource(
                "agents",
                &format!("intermediate-{:03}", i),
                format!(
                    r#"---
dependencies:
  agents:
    - path: base-{:03}.md
      version: v1.0.0
---
# Intermediate dependency {}

This depends on base-{:03}.
"#,
                    base_dep, base_dep, base_dep
                )
                .as_str(),
            )
            .await?;
    }

    // Add top-level dependencies that depend on intermediate ones (50)
    for i in 0..50 {
        let intermediate_dep = i % 40;
        let intermediate_name = format!("intermediate-{:03}", intermediate_dep);
        stress_repo
            .add_resource(
                "agents",
                &format!("top-level-{:03}", i),
                format!(
                    r#"---
dependencies:
  agents:
    - path: {}.md
      version: v1.0.0
---
# Top Level dependency {}

This depends on {}.
"#,
                    intermediate_name, i, intermediate_name
                )
                .as_str(),
            )
            .await?;
    }

    stress_repo.commit_all("Add stress test dependencies")?;
    stress_repo.tag_version("v1.0.0")?;

    // Create manifest with 50 top-level dependencies (will create 110 total with transitive deps)
    let source_url = stress_repo.bare_file_url(project.sources_path()).await?;
    let mut builder = ManifestBuilder::new().add_source("stress", &source_url);

    for i in 0..50 {
        builder = builder.add_agent(&format!("stress-{:03}", i), |d| {
            d.source("stress").path(&format!("agents/top-level-{:03}.md", i)).version("v1.0.0")
        });
    }

    let manifest = builder.build();
    project.write_manifest(&manifest).await?;

    // Measure performance
    let start_time = Instant::now();

    // Run install
    let output = project.run_agpm(&["install"])?;
    assert!(output.success, "Install should succeed. Stderr: {}", output.stderr);

    let install_duration = start_time.elapsed();

    // Verify installations
    let agents_dir = project.project_path().join(".claude/agents/agpm");
    let mut installed_count = 0;
    let mut entries = tokio::fs::read_dir(&agents_dir).await?;
    while let Some(_entry) = entries.next_entry().await? {
        installed_count += 1;
    }

    // Should have installed many files (50 top-level + transitive)
    assert!(
        installed_count >= 50,
        "Should have at least 50 installed files, got {}",
        installed_count
    );

    // Performance should be reasonable for concurrent processing
    let files_per_second = installed_count as f64 / install_duration.as_secs_f64();

    println!(
        "✅ Scale test: {} files installed in {:?} ({:.1} files/sec)",
        installed_count, install_duration, files_per_second
    );

    // Very generous warning thresholds (5-10x the original limits)
    if install_duration.as_secs() > 300 {
        eprintln!(
            "⚠️  Warning: Scale test took unusually long ({:?}), may indicate performance regression",
            install_duration
        );
    }

    if files_per_second < 0.5 {
        eprintln!(
            "⚠️  Warning: Very low throughput ({:.1} files/sec), may indicate performance issue",
            files_per_second
        );
    }

    // Verify specific files exist
    for i in 0..20 {
        let expected_file = format!("base-{:03}.md", i);
        let file_path = agents_dir.join(&expected_file);
        assert!(file_path.exists(), "Base dependency {} should be installed", expected_file);
    }

    Ok(())
}

/// Test deterministic concurrent resolution - multiple parallel identical installs
#[tokio::test]
async fn test_deterministic_concurrent_resolution() -> Result<()> {
    agpm_cli::test_utils::init_test_logging(None);

    // Create source repo with consistent dependency graph
    let project = TestProject::new().await?;
    let source_repo = project.create_source_repo("deterministic").await?;

    // Add base dependencies
    for i in 0..5 {
        source_repo
            .add_resource(
                "agents",
                &format!("base-{:02}", i),
                r#"---
# Base dependency
This is a consistent base dependency.
---
"#,
            )
            .await?;
    }

    // Add agents with transitive dependencies
    for i in 0..8 {
        let base_idx_1 = (i * 2) % 5;
        let base_idx_2 = (i * 2 + 1) % 5;

        source_repo
            .add_resource(
                "agents",
                &format!("agent-{:02}", i),
                format!(
                    r#"---
dependencies:
  agents:
    - path: base-{:02}.md
      version: v1.0.0
    - path: base-{:02}.md
      version: v1.0.0
---
# Agent {:02}

Agent with transitive dependencies on base-{:02} and base-{:02}.
"#,
                    base_idx_1, base_idx_2, i, base_idx_1, base_idx_2
                )
                .as_str(),
            )
            .await?;
    }

    source_repo.commit_all("Add deterministic test dependencies")?;
    source_repo.tag_version("v1.0.0")?;

    // Create manifest with all agents using the working project
    let source_url = source_repo.bare_file_url(project.sources_path()).await?;

    // Create manifest with all agents
    let mut builder = ManifestBuilder::new().add_source("deterministic", &source_url);
    for i in 0..8 {
        builder = builder.add_agent(&format!("agent-{:02}", i), |d| {
            d.source("deterministic").path(&format!("agents/agent-{:02}.md", i)).version("v1.0.0")
        });
    }
    let manifest = builder.build();
    project.write_manifest(&manifest).await?;

    // Run multiple installs on the same project to test deterministic behavior
    let mut lockfiles = Vec::new();

    for run_id in 0..3 {
        println!("Running deterministic install {}...", run_id + 1);

        // Run install - this will exercise the resolver's concurrent processing internally
        let output = project.run_agpm(&["install"])?;

        assert!(output.success, "Install {} should succeed. Stderr: {}", run_id, output.stderr);

        // Read the generated lockfile
        let lockfile_content = project.read_lockfile().await?;
        lockfiles.push(lockfile_content);
    }

    // Should have all successful results
    assert_eq!(lockfiles.len(), 3, "All 3 installs should succeed");

    // Normalize lockfiles by removing timestamps before comparing
    let normalize = |s: &str| {
        s.lines()
            .filter(|line| !line.trim().starts_with("fetched_at"))
            .collect::<Vec<_>>()
            .join("\n")
    };

    let normalized_first = normalize(&lockfiles[0]);
    for (i, lockfile) in lockfiles.iter().enumerate().skip(1) {
        let normalized_current = normalize(lockfile);
        assert_eq!(
            normalized_first,
            normalized_current,
            "Lockfile {} differs from first. This indicates non-deterministic resolution.",
            i + 1
        );
    }

    println!("✅ All 5 installs produced identical lockfiles");

    // Verify the lockfile contains expected dependencies
    for i in 0..8 {
        assert!(
            normalized_first.contains(&format!("agent-{:02}", i)),
            "Lockfile should contain agent-{:02}",
            i
        );
    }

    // Should have some base dependencies as transitive deps
    let mut base_dep_count = 0;
    for i in 0..5 {
        if normalized_first.contains(&format!("base-{:02}", i)) {
            base_dep_count += 1;
        }
    }
    assert!(base_dep_count > 0, "Should have some base dependencies in lockfile");

    println!(
        "✅ Deterministic concurrent resolution verified - {} base dependencies found",
        base_dep_count
    );

    Ok(())
}