Skip to content

Commit 145f2db

Browse files
mikolalysenkoclaude
andcommitted
test(setup): add nested-workspace + polyglot-monorepo layouts
Extends the setup-flow matrix with an `SM_LAYOUT` dimension modelling real-world deployments beyond a single project: - workspace (npm, pnpm, yarn, pip, uv): a root + several members, including a deeply-nested member and one with no dependency on the patched package. Exercises `setup`'s workspace handling — npm/yarn write the hook to every member, pnpm only to the root — and the cross-workspace apply on a single root install. npm/pnpm/yarn apply (the dependency hoists / lands in the pnpm store and is patched once); pip (nested requirements) and uv (uv workspace, one shared .venv) are Python gaps. - monorepo: a polyglot repo with an npm workspace alongside python/rust/go/php/ruby/nuget/deno manifests. Confirms `setup` works in a mixed environment — it configures the npm hooks and does not choke on the foreign manifests; a root `npm install` then patches the npm slice. Runs in the npm image; the foreign manifests are present to test setup's robustness, not installed. Wiring: `matrix.json` gains workspace_targets/scenarios and monorepo_targets/scenarios; `run-case.sh` gains layout-aware scaffold / install / multi-target verification; `scripts/setup-matrix.sh` threads a `layout` column (+ `query --layout`); the Rust harness gains `run_workspace_pm` / `run_monorepo`, with `*_workspace` tests on the npm/pypi wrappers and a new `setup_matrix_monorepo.rs`. Real-world finding (and fix in the harness): the install hook's `apply` must run with the package manager's per-script cwd — root for the project, the member dir for each member — so member postinstalls find no manifest and no-op while the root applies. The driver therefore does NOT pin SOCKET_CWD; pinning it to the root makes every member apply target the root manifest and fail mid-install with "no packages found on disk", breaking `npm install` in a workspace. Verified in Docker (socket-patch 3.3.0): npm/pnpm/yarn workspace and the monorepo apply (pass); pip/uv workspace are known_gap; single-project cases unchanged. 92 cases total; 0 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7049eec commit 145f2db

8 files changed

Lines changed: 467 additions & 76 deletions

File tree

crates/socket-patch-cli/tests/setup_matrix_common/mod.rs

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ struct Case {
9494
apply_ecosystems: String,
9595
marker: String,
9696
alt_marker: String,
97+
layout: String,
9798
}
9899

99100
impl Case {
@@ -121,29 +122,40 @@ impl Case {
121122
("SM_APPLY_ECOSYSTEMS".into(), self.apply_ecosystems.clone()),
122123
("SM_MARKER".into(), self.marker.clone()),
123124
("SM_ALT_MARKER".into(), self.alt_marker.clone()),
125+
("SM_LAYOUT".into(), self.layout.clone()),
124126
]
125127
}
126128
}
127129

128-
/// Load every case for a given (ecosystem, pm) by crossing that target
129-
/// with all scenarios in the spec.
130-
fn load_cases(ecosystem: &str, pm: &str) -> Vec<Case> {
130+
/// Load every case for a given (ecosystem, pm) by crossing the matching
131+
/// target in `targets_key` with every scenario in `scenarios_key`,
132+
/// tagging each with `layout`. `targets_key`/`scenarios_key` select the
133+
/// spec section: ("targets","scenarios") for single projects,
134+
/// ("workspace_targets","workspace_scenarios") for nested workspaces,
135+
/// ("monorepo_targets","monorepo_scenarios") for the polyglot monorepo.
136+
fn load_section(
137+
targets_key: &str,
138+
scenarios_key: &str,
139+
layout: &str,
140+
ecosystem: &str,
141+
pm: &str,
142+
) -> Vec<Case> {
131143
let text = std::fs::read_to_string(matrix_path())
132144
.unwrap_or_else(|e| panic!("read matrix.json: {e}"));
133145
let spec: serde_json::Value =
134146
serde_json::from_str(&text).expect("parse matrix.json");
135147
let marker = spec["marker"].as_str().unwrap_or("").to_string();
136148
let alt_marker = spec["alt_marker"].as_str().unwrap_or("").to_string();
137149

138-
let target = spec["targets"]
150+
let target = spec[targets_key]
139151
.as_array()
140-
.expect("targets array")
152+
.unwrap_or_else(|| panic!("{targets_key} array missing"))
141153
.iter()
142154
.find(|t| t["ecosystem"] == ecosystem && t["pm"] == pm)
143-
.unwrap_or_else(|| panic!("no target for {ecosystem}/{pm} in matrix.json"));
155+
.unwrap_or_else(|| panic!("no {targets_key} entry for {ecosystem}/{pm}"));
144156

145157
let mut cases = Vec::new();
146-
for s in spec["scenarios"].as_array().expect("scenarios array") {
158+
for s in spec[scenarios_key].as_array().expect("scenarios array") {
147159
let scenario = s["id"].as_str().unwrap().to_string();
148160
cases.push(Case {
149161
id: format!("{ecosystem}/{pm}/{scenario}"),
@@ -162,6 +174,7 @@ fn load_cases(ecosystem: &str, pm: &str) -> Vec<Case> {
162174
apply_ecosystems: target["apply_ecosystems"].as_str().unwrap().to_string(),
163175
marker: marker.clone(),
164176
alt_marker: alt_marker.clone(),
177+
layout: layout.to_string(),
165178
});
166179
}
167180
cases
@@ -222,22 +235,44 @@ fn run_case(case: &Case) -> RunResult {
222235
}
223236
}
224237

225-
/// Run every scenario for one (ecosystem, pm) and assert each meets the
226-
/// ASPIRATIONAL expectation. Soft-skips when Docker / the ecosystem
227-
/// image is unavailable (container mode) — matching the `docker_e2e_*`
228-
/// convention where Rust integration tests have no native "skipped".
238+
/// Run the single-project scenarios for one (ecosystem, pm).
229239
pub fn run_pm(ecosystem: &str, pm: &str) {
240+
run_cases(
241+
&format!("{ecosystem}/{pm}"),
242+
load_section("targets", "scenarios", "single", ecosystem, pm),
243+
);
244+
}
245+
246+
/// Run the nested-workspace scenarios for one (ecosystem, pm).
247+
pub fn run_workspace_pm(ecosystem: &str, pm: &str) {
248+
run_cases(
249+
&format!("{ecosystem}/{pm} [workspace]"),
250+
load_section("workspace_targets", "workspace_scenarios", "workspace", ecosystem, pm),
251+
);
252+
}
253+
254+
/// Run the polyglot all-ecosystem monorepo scenarios.
255+
pub fn run_monorepo() {
256+
run_cases(
257+
"monorepo",
258+
load_section("monorepo_targets", "monorepo_scenarios", "monorepo", "monorepo", "mono"),
259+
);
260+
}
261+
262+
/// Execute a set of cases and assert each meets the ASPIRATIONAL
263+
/// expectation. Soft-skips when Docker / the ecosystem image is
264+
/// unavailable (container mode) — matching the `docker_e2e_*` convention
265+
/// where Rust integration tests have no native "skipped".
266+
fn run_cases(label: &str, cases: Vec<Case>) {
230267
if !host_mode() && !docker_on_path() {
231-
eprintln!("skip {ecosystem}/{pm}: docker not on PATH (set SOCKET_PATCH_TEST_HOST=1 to run on host)");
268+
eprintln!("skip {label}: docker not on PATH (set SOCKET_PATCH_TEST_HOST=1 to run on host)");
232269
return;
233270
}
234-
235-
let cases = load_cases(ecosystem, pm);
236271
if !host_mode() {
237272
if let Some(c) = cases.first() {
238273
if !image_present(&c.image) {
239274
eprintln!(
240-
"skip {ecosystem}/{pm}: image socket-patch-test-{}:latest not present \
275+
"skip {label}: image socket-patch-test-{}:latest not present \
241276
(build it: scripts/setup-matrix.sh build --ecosystem {})",
242277
c.image, c.image
243278
);
@@ -267,12 +302,11 @@ pub fn run_pm(ecosystem: &str, pm: &str) {
267302

268303
assert!(
269304
failures.is_empty(),
270-
"{}/{}: {} of {} setup-matrix case(s) did not meet the aspirational \
305+
"{}: {} of {} setup-matrix case(s) did not meet the aspirational \
271306
expectation. BASELINE GAP entries are the experimental TODO list \
272307
(this suite is non-blocking in CI); REGRESSION / LEAK entries are \
273308
real problems:\n{}",
274-
ecosystem,
275-
pm,
309+
label,
276310
failures.len(),
277311
cases.len(),
278312
failures.join("\n")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! setup-matrix: polyglot all-ecosystem monorepo.
2+
//!
3+
//! A single repo containing an npm workspace alongside
4+
//! python/rust/go/php/ruby/nuget/deno manifests. Confirms `socket-patch
5+
//! setup` works in this mixed environment — it must configure the npm
6+
//! hooks and NOT choke on the foreign manifests; a root `npm install`
7+
//! then applies the patch to the npm slice. Runs in the npm image (the
8+
//! only one with the npm toolchain); the foreign manifests are present
9+
//! to test setup's robustness, not installed.
10+
//!
11+
//! Run: `cargo test -p socket-patch-cli --features setup-e2e --test setup_matrix_monorepo`
12+
#![cfg(feature = "setup-e2e")]
13+
14+
#[path = "setup_matrix_common/mod.rs"]
15+
mod smc;
16+
17+
#[test]
18+
fn monorepo() {
19+
smc::run_monorepo();
20+
}

crates/socket-patch-cli/tests/setup_matrix_npm.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,25 @@ fn pnpm() {
3131
fn bun() {
3232
smc::run_pm("npm", "bun");
3333
}
34+
35+
// ── Nested-workspace layouts ──────────────────────────────────────────
36+
// A root + several members (incl. a deeply-nested one and a member with
37+
// no dependency on the patched package). Exercises `setup`'s workspace
38+
// handling (npm/yarn write the hook to every member; pnpm only to the
39+
// root) plus the cross-workspace apply on the root install. These should
40+
// PASS — they're real regression guards, not gap documentation.
41+
42+
#[test]
43+
fn npm_workspace() {
44+
smc::run_workspace_pm("npm", "npm");
45+
}
46+
47+
#[test]
48+
fn pnpm_workspace() {
49+
smc::run_workspace_pm("npm", "pnpm");
50+
}
51+
52+
#[test]
53+
fn yarn_workspace() {
54+
smc::run_workspace_pm("npm", "yarn");
55+
}

crates/socket-patch-cli/tests/setup_matrix_pypi.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,18 @@ fn pdm() {
3535
fn hatch() {
3636
smc::run_pm("pypi", "hatch");
3737
}
38+
39+
// ── Nested-workspace layouts (EXPECTED BASELINE GAP) ──────────────────
40+
// uv workspace (root + members, one shared .venv) and a pip
41+
// nested-requirements monorepo. Python has no post-install hook, so
42+
// these don't apply today — but the install itself must succeed.
43+
44+
#[test]
45+
fn pip_workspace() {
46+
smc::run_workspace_pm("pypi", "pip");
47+
}
48+
49+
#[test]
50+
fn uv_workspace() {
51+
smc::run_workspace_pm("pypi", "uv");
52+
}

scripts/setup-matrix.sh

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,29 @@ usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; }
4747
need jq
4848
[ -f "$MATRIX" ] || die "matrix spec not found: $MATRIX"
4949

50-
# Emit one TSV row per case (target x scenario), honoring filters.
50+
# Emit one TSV row per case, honoring filters. Covers all three layouts:
51+
# single (targets x scenarios), workspace (workspace_targets x
52+
# workspace_scenarios) and monorepo (monorepo_targets x monorepo_scenarios).
5153
# Columns: id eco pm image hook_family baseline_supported package version
5254
# purl manifest_key apply_ecosystems scenario patchset run_setup
53-
# expect_applied
55+
# expect_applied layout
5456
cases_tsv() { # $1=eco-filter ("" = all) $2=pm-filter $3=scenario-filter
5557
jq -r --arg eco "${1:-}" --arg pm "${2:-}" --arg scn "${3:-}" '
56-
.marker as $m | .alt_marker as $am |
57-
.targets[] as $t | .scenarios[] as $s |
58-
select($eco == "" or $t.ecosystem == $eco) |
59-
select($pm == "" or $t.pm == $pm) |
60-
select($scn == "" or $s.id == $scn) |
61-
[ ($t.ecosystem + "/" + $t.pm + "/" + $s.id),
62-
$t.ecosystem, $t.pm, $t.image, $t.hook_family,
63-
($t.baseline_supported|tostring),
64-
$t.package, $t.version, $t.purl, $t.manifest_key, $t.apply_ecosystems,
65-
$s.id, $s.patchset, ($s.run_setup|tostring), ($s.expect_applied|tostring)
66-
] | @tsv
58+
def rows($targets; $scenarios; $layout):
59+
$targets[] as $t | $scenarios[] as $s
60+
| select($eco == "" or $t.ecosystem == $eco)
61+
| select($pm == "" or $t.pm == $pm)
62+
| select($scn == "" or $s.id == $scn)
63+
| [ ($t.ecosystem + "/" + $t.pm + "/" + $s.id),
64+
$t.ecosystem, $t.pm, $t.image, ($t.hook_family // ""),
65+
($t.baseline_supported|tostring),
66+
$t.package, $t.version, $t.purl, $t.manifest_key, $t.apply_ecosystems,
67+
$s.id, $s.patchset, ($s.run_setup|tostring), ($s.expect_applied|tostring),
68+
$layout ]
69+
| @tsv;
70+
rows(.targets; .scenarios; "single"),
71+
rows((.workspace_targets // []); (.workspace_scenarios // []); "workspace"),
72+
rows((.monorepo_targets // []); (.monorepo_scenarios // []); "monorepo")
6773
' "$MATRIX"
6874
}
6975

@@ -101,9 +107,9 @@ cmd_list() {
101107
scenario:$s.id, image:$t.image, hook_family:$t.hook_family,
102108
baseline_supported:$t.baseline_supported, expect_applied:$s.expect_applied } ]' "$MATRIX"
103109
else
104-
printf '%-44s %-9s %-8s %-22s %s\n' ID ECO PM SCENARIO EXPECT
105-
cases_tsv "" "" "" | while IFS=$'\t' read -r id eco pm image hook bsup pkg ver purl key aeco scn pset rsetup expect; do
106-
printf '%-44s %-9s %-8s %-22s %s\n' "$id" "$eco" "$pm" "$scn" "$expect"
110+
printf '%-46s %-9s %-8s %-11s %-22s %s\n' ID ECO PM LAYOUT SCENARIO EXPECT
111+
cases_tsv "" "" "" | while IFS=$'\t' read -r id eco pm image hook bsup pkg ver purl key aeco scn pset rsetup expect layout; do
112+
printf '%-46s %-9s %-8s %-11s %-22s %s\n' "$id" "$eco" "$pm" "$layout" "$scn" "$expect"
107113
done
108114
fi
109115
}
@@ -142,14 +148,15 @@ cmd_run() {
142148
fi
143149

144150
local total=0
145-
while IFS=$'\t' read -r id eco_ pm_ image hook bsup pkg ver purl key aeco scn_ pset rsetup expect; do
151+
while IFS=$'\t' read -r id eco_ pm_ image hook bsup pkg ver purl key aeco scn_ pset rsetup expect layout; do
146152
[ -z "$id" ] && continue
147153
total=$((total+1))
148-
echo ">> [$total] $id" >&2
154+
echo ">> [$total] $id (layout=$layout)" >&2
149155

150156
# Common SM_* env for the driver.
151157
local -a base_env=(
152158
"SM_ID=$id" "SM_ECOSYSTEM=$eco_" "SM_PM=$pm_" "SM_SCENARIO=$scn_"
159+
"SM_LAYOUT=$layout"
153160
"SM_PATCHSET=$pset" "SM_RUN_SETUP=$([ "$rsetup" = true ] && echo 1 || echo 0)"
154161
"SM_EXPECT_APPLIED=$([ "$expect" = true ] && echo 1 || echo 0)"
155162
"SM_PACKAGE=$pkg" "SM_VERSION=$ver" "SM_PURL=$purl"
@@ -182,23 +189,23 @@ cmd_run() {
182189
if [ "$expect" = true ] && [ "$bsup" = true ]; then bl=true; fi
183190

184191
if [ -n "$result" ] && printf '%s' "$result" | jq -e . >/dev/null 2>&1; then
185-
printf '%s\n' "$result" | jq -c --argjson bl "$bl" --arg img "$image" --arg hk "$hook" '
192+
printf '%s\n' "$result" | jq -c --argjson bl "$bl" --arg img "$image" --arg hk "$hook" --arg lay "$layout" '
186193
. as $r |
187194
($r.actual_applied == $r.expect_applied) as $ideal |
188195
($r.actual_applied == $bl) as $base |
189196
(if $ideal and $base then "pass"
190197
elif $ideal and ($base|not) then "progress"
191198
elif ($ideal|not) and $base then "known_gap"
192199
else "regression" end) as $cls |
193-
$r + {baseline_applied:$bl, classification:$cls, image:$img, hook_family:$hk, driver_rc:'"$rc"'}
200+
$r + {baseline_applied:$bl, classification:$cls, layout:$lay, image:$img, hook_family:$hk, driver_rc:'"$rc"'}
194201
' >> "$jsonl"
195202
else
196203
# No parseable result — surface as an error case.
197204
jq -nc --arg id "$id" --arg eco "$eco_" --arg pm "$pm_" --arg scn "$scn_" \
198-
--arg pset "$pset" --arg img "$image" --arg hk "$hook" --argjson bl "$bl" '
205+
--arg pset "$pset" --arg img "$image" --arg hk "$hook" --arg lay "$layout" --argjson bl "$bl" '
199206
{ id:$id, ecosystem:$eco, pm:$pm, scenario:$scn, patchset:$pset,
200207
expect_applied:null, actual_applied:null, baseline_applied:$bl,
201-
classification:"error", image:$img, hook_family:$hk, driver_rc:'"$rc"',
208+
classification:"error", layout:$lay, image:$img, hook_family:$hk, driver_rc:'"$rc"',
202209
notes:"driver produced no parseable result" }' >> "$jsonl"
203210
fi
204211
done < <(cases_tsv "$eco" "$pm" "$scn")
@@ -239,21 +246,23 @@ print_summary() { # $1 = results file
239246

240247
# --------------------------------------------------------------------- query / results
241248
cmd_query() {
242-
local status="" eco="" pm="" scn=""
249+
local status="" eco="" pm="" scn="" lay=""
243250
while [ $# -gt 0 ]; do case "$1" in
244251
--status) status="$2"; shift 2;;
245252
--ecosystem) eco="$2"; shift 2;;
246253
--pm) pm="$2"; shift 2;;
247254
--scenario) scn="$2"; shift 2;;
255+
--layout) lay="$2"; shift 2;;
248256
*) die "query: unknown arg '$1'";;
249257
esac; done
250258
[ -f "$LATEST" ] || die "no results yet — run '$0 run' first"
251-
jq --arg st "$status" --arg eco "$eco" --arg pm "$pm" --arg scn "$scn" '
259+
jq --arg st "$status" --arg eco "$eco" --arg pm "$pm" --arg scn "$scn" --arg lay "$lay" '
252260
[ .cases[]
253261
| select($st == "" or .classification == $st)
254262
| select($eco == "" or .ecosystem == $eco)
255263
| select($pm == "" or .pm == $pm)
256-
| select($scn == "" or .scenario == $scn) ]' "$LATEST"
264+
| select($scn == "" or .scenario == $scn)
265+
| select($lay == "" or .layout == $lay) ]' "$LATEST"
257266
}
258267

259268
cmd_results() {

0 commit comments

Comments
 (0)