Skip to content

Commit 5c2b1aa

Browse files
committed
Carry opt-in fields through check output
Register manifest and detached vendor metadata for both check JSON envelopes, including already-patched and drift events. Represent detached vendor state in manifest-free JSON without changing unrelated empty applies. Keep human output deduplicated by UUID and note, and leave direct UUID dry-run rendering to its preview layer. Extend the output matrix with human and JSON assertions for every mode and operation. Assisted-by: Claude Code:kimi-k3p1-pro
1 parent a5b042f commit 5c2b1aa

3 files changed

Lines changed: 128 additions & 28 deletions

File tree

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
422422
Err(e) => return refuse_unreadable_manifest(args, manifest_path, &e),
423423
};
424424

425+
let mut opt_ins = opt_in_patches(&manifest);
426+
opt_ins.extend(vendor_opt_in_patches(&args.common).await);
427+
425428
// (purl_or_name, reason_code, detail) for each drift.
426429
let mut drifts: Vec<(String, String, String)> = Vec::new();
427430
let mut checked: usize = 0;
@@ -604,23 +607,22 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
604607
}
605608

606609
if !args.common.json && !args.common.silent {
607-
if let Some(notes) =
608-
crate::ui::opt_in_note_block(manifest.patches.iter().map(|(purl, record)| {
609-
crate::ui::OptInEntry {
610-
purl,
611-
uuid: &record.uuid,
612-
requires_opt_in: record.requires_opt_in,
613-
opt_in_note: record.opt_in_note.as_deref(),
614-
}
615-
}))
616-
{
610+
if let Some(notes) = crate::ui::opt_in_note_block(opt_ins.iter().map(
611+
|(purl, uuid, requires_opt_in, opt_in_note)| crate::ui::OptInEntry {
612+
purl,
613+
uuid,
614+
requires_opt_in: *requires_opt_in,
615+
opt_in_note: opt_in_note.as_deref(),
616+
},
617+
)) {
617618
println!("\n{notes}");
618619
}
619620
}
620621

621622
if drifts.is_empty() {
622623
if args.common.json {
623624
let mut env = Envelope::new(Command::Apply);
625+
register_opt_ins(&mut env, &opt_ins);
624626
record_check_skips(&mut env, &in_sync, &not_installed);
625627
for event in unchecked_events {
626628
env.record(event);
@@ -637,6 +639,7 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
637639
} else {
638640
if args.common.json {
639641
let mut env = Envelope::new(Command::Apply);
642+
register_opt_ins(&mut env, &opt_ins);
640643
for (id, code, detail) in &drifts {
641644
env.record(
642645
PatchEvent::new(PatchAction::Failed, id.clone())
@@ -1305,6 +1308,21 @@ pub async fn run(args: ApplyArgs) -> i32 {
13051308
env.status = Status::NoManifest;
13061309
env.dry_run = args.common.dry_run;
13071310
env.warnings.extend(below.warnings());
1311+
let vendor_patches = vendor_opt_in_patches(&args.common).await;
1312+
for (purl, uuid, requires_opt_in, opt_in_note) in &vendor_patches {
1313+
if requires_opt_in != &Some(true) && opt_in_note.is_none() {
1314+
continue;
1315+
}
1316+
env.register_opt_in(purl, uuid, *requires_opt_in, opt_in_note.as_deref());
1317+
env.record(
1318+
PatchEvent::new(PatchAction::Skipped, purl.clone())
1319+
.with_uuid(uuid.clone())
1320+
.with_reason(
1321+
"already_vendored",
1322+
"vendored patch is recorded in .socket/vendor/state.json",
1323+
),
1324+
);
1325+
}
13081326
match &vex_result {
13091327
Some(ManifestlessVex::Written(summary)) => {
13101328
env.vex = Some(VexSummary {
@@ -1519,21 +1537,19 @@ fn collect_apply_outcomes(
15191537
}
15201538

15211539
async fn print_vendor_opt_in_notes(common: &GlobalArgs) {
1522-
if common.json || common.silent || common.is_global() {
1540+
if common.json || common.silent {
15231541
return;
15241542
}
1525-
let Ok(state) = socket_patch_core::vendor::load_state(&common.cwd).await else {
1526-
return;
1527-
};
1528-
let entries = state.entries.values().filter_map(|entry| {
1529-
let record = entry.record.as_ref()?;
1530-
Some(crate::ui::OptInEntry {
1531-
purl: &entry.base_purl,
1532-
uuid: &record.uuid,
1533-
requires_opt_in: record.requires_opt_in,
1534-
opt_in_note: record.opt_in_note.as_deref(),
1535-
})
1536-
});
1543+
let patches = vendor_opt_in_patches(common).await;
1544+
let entries =
1545+
patches.iter().map(
1546+
|(purl, uuid, requires_opt_in, opt_in_note)| crate::ui::OptInEntry {
1547+
purl,
1548+
uuid,
1549+
requires_opt_in: *requires_opt_in,
1550+
opt_in_note: opt_in_note.as_deref(),
1551+
},
1552+
);
15371553
if let Some(notes) = crate::ui::opt_in_note_block(entries) {
15381554
println!("\n{notes}");
15391555
}
@@ -1556,6 +1572,34 @@ fn opt_in_patches(manifest: &PatchManifest) -> OptInPatches {
15561572
.collect()
15571573
}
15581574

1575+
async fn vendor_opt_in_patches(common: &GlobalArgs) -> OptInPatches {
1576+
if common.is_global() {
1577+
return Vec::new();
1578+
}
1579+
let Ok(state) = socket_patch_core::vendor::load_state(&common.cwd).await else {
1580+
return Vec::new();
1581+
};
1582+
state
1583+
.entries
1584+
.into_iter()
1585+
.filter_map(|(purl, entry)| {
1586+
let record = entry.record?;
1587+
Some((
1588+
purl,
1589+
record.uuid,
1590+
record.requires_opt_in,
1591+
record.opt_in_note,
1592+
))
1593+
})
1594+
.collect()
1595+
}
1596+
1597+
fn register_opt_ins(env: &mut Envelope, patches: &OptInPatches) {
1598+
for (purl, uuid, requires_opt_in, opt_in_note) in patches {
1599+
env.register_opt_in(purl, uuid, *requires_opt_in, opt_in_note.as_deref());
1600+
}
1601+
}
1602+
15591603
/// The locked half of `apply`: everything from the manifest read on — the
15601604
/// package-manager layout gate, the apply loop, embedded VEX, output and
15611605
/// telemetry — over a `lock` the caller already holds and the caller's
@@ -1612,7 +1656,8 @@ pub(crate) async fn run_locked(
16121656

16131657
// Packages the project patched itself sit this run out (warned); every
16141658
// other record still applies. The read-only `--check` verifies them all.
1615-
let opt_ins = opt_in_patches(&manifest);
1659+
let mut opt_ins = opt_in_patches(&manifest);
1660+
opt_ins.extend(vendor_opt_in_patches(&args.common).await);
16161661
let mut manifest = manifest;
16171662
let (user_patch_warnings, user_patch_skips) = if args.check {
16181663
Default::default()
@@ -1937,9 +1982,7 @@ pub(crate) async fn run_locked(
19371982
if args.common.json {
19381983
let mut env = Envelope::new(Command::Apply);
19391984
env.dry_run = args.common.dry_run;
1940-
for (purl, uuid, requires_opt_in, opt_in_note) in &opt_ins {
1941-
env.register_opt_in(purl, uuid, *requires_opt_in, opt_in_note.as_deref());
1942-
}
1985+
register_opt_ins(&mut env, &opt_ins);
19431986
for skip in &user_patch_skips {
19441987
env.record(skip.clone());
19451988
}

‎crates/socket-patch-cli/src/commands/get.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,7 +1356,7 @@ pub async fn run(args: GetArgs) -> i32 {
13561356
.await
13571357
}
13581358
};
1359-
if !quiet && mode == super::scan::ScanMode::Agent {
1359+
if !quiet && !args.common.dry_run && mode == super::scan::ScanMode::Agent {
13601360
if let Some(notes) =
13611361
crate::ui::opt_in_note_block([crate::ui::OptInEntry::from_response(&patch)])
13621362
{

‎crates/socket-patch-cli/tests/opt_in_output_matrix.rs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enum Action {
3333
Real,
3434
DryRun,
3535
SaveOnly,
36+
Check,
3637
}
3738

3839
impl CommandKind {
@@ -62,6 +63,7 @@ impl Action {
6263
Self::Real => "real",
6364
Self::DryRun => "dry-run",
6465
Self::SaveOnly => "save-only",
66+
Self::Check => "check",
6567
}
6668
}
6769
}
@@ -165,6 +167,7 @@ fn get_args(server: &MockServer, mode: Mode, action: Action) -> Vec<String> {
165167
Action::Real => {}
166168
Action::DryRun => args.push("--dry-run".into()),
167169
Action::SaveOnly => args.push("--save-only".into()),
170+
Action::Check => args.push("--check".into()),
168171
}
169172
args
170173
}
@@ -206,6 +209,11 @@ async fn stage_state(
206209
AFTER_BYTES,
207210
)
208211
.unwrap();
212+
std::fs::write(
213+
root.join("node_modules").join(NAME).join("index.js"),
214+
AFTER_BYTES,
215+
)
216+
.unwrap();
209217
}
210218
}
211219

@@ -222,10 +230,26 @@ fn opt_in_output_matrix() {
222230
for action in [Action::Real, Action::DryRun, Action::SaveOnly] {
223231
runtime.block_on(run_case(command, mode, action));
224232
}
233+
if matches!(command, CommandKind::Apply) {
234+
runtime.block_on(run_case(command, mode, Action::Check));
235+
}
225236
}
226237
}
227238
}
228239

240+
fn assert_json_opt_in(stdout: &str, stderr: &str, label: &str) {
241+
let doc: serde_json::Value = serde_json::from_str(stdout)
242+
.unwrap_or_else(|e| panic!("{label}: JSON {e}; stdout={stdout}\nstderr={stderr}"));
243+
let event = doc["events"]
244+
.as_array()
245+
.unwrap()
246+
.iter()
247+
.find(|event| event["uuid"] == UUID || event["purl"] == PURL)
248+
.unwrap_or_else(|| panic!("{label}: no matching event in {doc}"));
249+
assert_eq!(event["details"]["requiresOptIn"], true, "{label}: {event}");
250+
assert_eq!(event["details"]["optInNote"], NOTE, "{label}: {event}");
251+
}
252+
229253
async fn run_case(command: CommandKind, mode: Mode, action: Action) {
230254
let valid = !matches!(action, Action::SaveOnly)
231255
|| matches!((command, mode), (CommandKind::Get, Mode::Agent));
@@ -252,7 +276,12 @@ async fn run_case(command: CommandKind, mode: Mode, action: Action) {
252276
args.push("--save-only".into());
253277
}
254278
}
279+
if matches!(command, CommandKind::Apply) && matches!(action, Action::Check) {
280+
args.push("--check".into());
281+
}
255282

283+
let mut json_args = args.clone();
284+
json_args.push("--json".into());
256285
let (code, stdout, stderr) = if matches!(command, CommandKind::Apply | CommandKind::List) {
257286
stage_state(tmp.path(), &server, mode, command).await;
258287
server.reset().await;
@@ -273,4 +302,32 @@ async fn run_case(command: CommandKind, mode: Mode, action: Action) {
273302
"{label} must not fetch patch metadata"
274303
);
275304
}
305+
306+
if matches!(action, Action::Check) && matches!(mode, Mode::Agent) {
307+
std::fs::write(
308+
tmp.path().join("node_modules").join(NAME).join("index.js"),
309+
BEFORE_BYTES,
310+
)
311+
.unwrap();
312+
}
313+
let (code, json, stderr) = run_argv(tmp.path(), json_args);
314+
if valid {
315+
let expected_code = if matches!(action, Action::Check) && matches!(mode, Mode::Agent) {
316+
1
317+
} else {
318+
0
319+
};
320+
assert_eq!(
321+
code, expected_code,
322+
"{label} JSON: stdout={json}\nstderr={stderr}"
323+
);
324+
assert_json_opt_in(&json, &stderr, &label);
325+
} else {
326+
assert_ne!(code, 0, "{label} JSON: stdout={json}\nstderr={stderr}");
327+
assert!(!json.contains(NOTE), "{label}: {json}");
328+
assert!(
329+
received_paths(&server).await.is_empty(),
330+
"{label} JSON must not fetch patch metadata"
331+
);
332+
}
276333
}

0 commit comments

Comments
 (0)