diff --git a/.jscpd-report.md b/.jscpd-report.md
new file mode 100644
index 00000000..61bb2eb3
--- /dev/null
+++ b/.jscpd-report.md
@@ -0,0 +1,659 @@
+# Duplication Analysis Report
+
+> Generated: 2026-04-07 | Tool: jscpd 4.0.8 | Threshold: 5.0%
+
+## Summary
+
+| Project | Files | Lines | Clones | Duplicated Lines | Percentage |
+|---------|------:|------:|-------:|-----------------:|-----------:|
+| babs | 77 | 12356 | 18 | 434 | 3.51% |
+
+> Duplication is within the 5.0% threshold for all projects.
+
+## Duplicate Clusters
+
+| C | Lines | %file | Difficulty | Strategy | Files |
+|----|-------|-------|------------|----------------------------------------------------|------------------------------------------------------------------------|
+| 1 | 86 | 50% | Moderate | Extract shared test fixture to conftest.py | test_generate_bidsapp_runscript.py / test_generate_submit_script.py |
+| 2 | 41 | 100% | Moderate | Extract shared test fixture to conftest.py | eg_simbids_0-0-3_multiinput.yaml / config_simbids_multiinput.yaml |
+| 3 | 34 | 20% | Moderate | Extract shared test fixture to conftest.py | test_generate_bidsapp_runscript.py / test_generate_submit_script.py |
+| 4 | 34 | 7% | Easy | Extract local helper function | generate_bidsapp_runscript.py |
+| 5 | 30 | 6% | Easy | Extract local helper function | generate_bidsapp_runscript.py |
+| 6 | 28 | 14% | Easy | Consolidate repeated sections within this document | whats_new.md |
+| 7 | 25 | 68% | Moderate | Extract shared test fixture to conftest.py | config_simbids.yaml / config_simbids_multiinput.yaml |
+| 8 | 24 | 9% | Moderate | Extract test fixture or parametrize | test_check_setup.py |
+| 9 | 23 | 58% | Moderate | Extract shared test fixture to conftest.py | config_simbids_multiinput.yaml / simbids_fmriprep-24-1-1_anatonly.yaml |
+| 10 | 18 | 7% | Moderate | Extract shared test fixture to conftest.py | test_babs_workflow.py / test_update_input_data.py |
+| 11 | 18 | 2% | Easy | Extract local helper function | cli.py |
+| 12 | 18 | 2% | Easy | Extract local helper function | cli.py |
+| 13 | 17 | 5% | Moderate | Extract shared test fixture to conftest.py | conftest.py / test_babs_workflow.py |
+| 14 | 14 | 5% | Moderate | Extract test fixture or parametrize | test_input_datasets.py |
+| 15 | 12 | 2% | Moderate | Extract test fixture or parametrize | test_utils.py |
+| 16 | 11 | 6% | Moderate | Extract test fixture or parametrize | test_interaction.py |
+| 17 | 10 | 6% | Easy | Extract test fixture or parametrize | test_interaction.py |
+| 18 | 9 | 2% | Easy | Extract local helper function | check_setup.py |
+
+
+Cluster 1: [Moderate] `tests/test_generate_bidsapp_runscript.py` lines 13-98 ↔ `tests/test_generate_submit_script.py` lines 8-93 (86 lines 50% of file)
+
+**Files involved:**
+- [`tests/test_generate_bidsapp_runscript.py` (lines 13-98)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_bidsapp_runscript.py#L13-L98)
+- [`tests/test_generate_submit_script.py` (lines 8-93)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_submit_script.py#L8-L93)
+
+**Duplicated fragment:**
+~~~python
+read_yaml,
+)
+
+input_datasets_prep = [
+ {
+ 'name': 'bids',
+ 'path_in_babs': 'inputs/data/BIDS',
+ 'unzipped_path_containing_subject_dirs': 'inputs/data/BIDS',
+ 'is_zipped': False,
+ },
+]
+
+input_datasets_fmriprep_ingressed_anat = [
+ {
+ 'name': 'freesurfer',
+ 'path_in_babs': 'inputs/data',
+ 'unzipped_path_containing_subject_dirs': 'inputs/data/freesurfer',
+ 'is_zipped': True,
+ },
+ {
+ 'name': 'bids',
+ 'path_in_babs': 'inputs/data/BIDS',
+ 'unzipped_path_containing_subject_dirs': 'inputs/data/BIDS',
+ 'is_zipped': False,
+ },
+]
+
+input_datasets_xcpd = [
+ {
+ 'name': 'fmriprep',
+... (56 more lines)
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 2: [Moderate] `notebooks/eg_simbids_0-0-3_multiinput.yaml` lines 1-41 ↔ `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 1-45 (41 lines 100% of file)
+
+**Files involved:**
+- [`notebooks/eg_simbids_0-0-3_multiinput.yaml` (lines 1-41)](https://github.com/PennLINC/babs/blob/main/notebooks/eg_simbids_0-0-3_multiinput.yaml#L1-L41)
+- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 1-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L1-L45)
+
+**Duplicated fragment:**
+~~~yaml
+input_datasets:
+ BIDS:
+ required_files:
+ - "anat/*_T1w.nii*"
+ is_zipped: false
+ origin_url: "/test-temp/simbids"
+ path_in_babs: inputs/data/BIDS
+ fmriprep_anat:
+ is_zipped: true
+ origin_url: "ria+file:///test-temp/test_project/output_ria#~data"
+ path_in_babs: inputs/data/fmriprep_anat
+
+bids_app_args:
+ --bids-app: "fmriprep"
+ $SUBJECT_SELECTION_FLAG: "--participant-label"
+ --stop-on-first-crash: ""
+ -vv: ""
+
+all_results_in_one_zip: true
+zip_foldernames:
+ fmriprep_anat: "24-1-1"
+
+singularity_args:
+ - --containall
+ - --writable-tmpfs
+
+cluster_resources:
+ interpreting_shell: "/bin/bash"
+
+script_preamble: |
+... (11 more lines)
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 3: [Moderate] `tests/test_generate_bidsapp_runscript.py` lines 134-167 ↔ `tests/test_generate_submit_script.py` lines 111-144 (34 lines)
+
+**Files involved:**
+- [`tests/test_generate_bidsapp_runscript.py` (lines 134-167)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_bidsapp_runscript.py#L134-L167)
+- [`tests/test_generate_submit_script.py` (lines 111-144)](https://github.com/PennLINC/babs/blob/main/tests/test_generate_submit_script.py#L111-L144)
+
+**Duplicated fragment:**
+~~~python
+with open(out_fn, 'w') as f:
+ f.write(script_content)
+ passed, status = run_shellcheck(str(out_fn))
+ if not passed:
+ print(script_content)
+ assert passed, status
+
+
+def run_shellcheck(script_path):
+ """Run shellcheck on a shell script string and return the result.
+
+ Parameters
+ ----------
+ script_path : str
+ The path to the shell script to check
+
+ Returns
+ -------
+ tuple
+ (bool, str) where bool indicates success (True) or failure (False),
+ and str contains shellcheck output
+ """
+
+ try:
+ # Run shellcheck on the temporary file
+ result = subprocess.run(['shellcheck', script_path], capture_output=True, text=True)
+ return result.returncode == 0, result.stdout
+ except subprocess.CalledProcessError as e:
+ return False, e.output
+ except Exception as e:
+... (4 more lines)
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 4: [Easy] `babs/generate_bidsapp_runscript.py` lines 290-323 ↔ `babs/generate_bidsapp_runscript.py` lines 222-255 (34 lines)
+
+**Files involved:**
+- [`babs/generate_bidsapp_runscript.py` (lines 290-323)](https://github.com/PennLINC/babs/blob/main/babs/generate_bidsapp_runscript.py#L290-L323)
+- [`babs/generate_bidsapp_runscript.py` (lines 222-255)](https://github.com/PennLINC/babs/blob/main/babs/generate_bidsapp_runscript.py#L222-L255)
+
+**Duplicated fragment:**
+~~~python
+)
+
+ else: # check on values:
+ if value in ('', None, 'Null', 'NULL'): # a flag, without value
+ cmds.append(str(key))
+ else: # a flag with value
+ # check if it is a placeholder which needs to be replaced:
+ # e.g., `$BABS_TMPDIR`
+ if value.startswith('$BABS_'):
+ value = replace_placeholder_from_config(value)
+
+ cmds.append(f'{key} {value}')
+
+ # Ensure that subject_selection_flag is not None before returning
+ if subject_selection_flag is None:
+ subject_selection_flag = '--participant-label'
+ print(
+ "'$SUBJECT_SELECTION_FLAG' not found in `bids_app_args` section of the YAML file. "
+ 'Using `--participant-label` as the default subject selection flag.'
+ )
+
+ # The input dataset is always the first one in the list
+ bids_app_input_dir = input_datasets[0]['unzipped_path_containing_subject_dirs']
+
+ return (
+ cmds,
+ subject_selection_flag,
+ flag_fs_license,
+ path_fs_license,
+ bids_app_input_dir,
+... (4 more lines)
+~~~
+
+**Mediation** (Easy → **Moderate**): Refactor `bids_app_args_from_config` and `bids_app_args_from_config_pipeline` into a single function
+
+> These two functions (`bids_app_args_from_config` at line 155 and `bids_app_args_from_config_pipeline` at line 255) are nearly identical. The only difference is how `$SESSION_SELECTION_FLAG` is handled: the original always appends `{value} $sesid`, while the pipeline version conditionally checks `processing_level == 'session'`. The fix is to merge them into one function that accepts an optional `processing_level` parameter (defaulting to `'session'` for backward compatibility). This covers both Clusters 4 and 5.
+
+
+
+
+Cluster 6: [Easy] `docs/whats_new.md` lines 169-196 ↔ `docs/whats_new.md` lines 112-137 (28 lines)
+
+**Files involved:**
+- [`docs/whats_new.md` (lines 169-196)](https://github.com/PennLINC/babs/blob/main/docs/whats_new.md#L169-L196)
+- [`docs/whats_new.md` (lines 112-137)](https://github.com/PennLINC/babs/blob/main/docs/whats_new.md#L112-L137)
+
+**Duplicated fragment:**
+~~~markdown
+nnLINC/babs/pull/194)
+* @singlesp made their first contribution in [#239](https://github.com/PennLINC/babs/pull/239)
+
+**Full Changelog**: [https://github.com/PennLINC/babs/compare/0.0.8...0.2.0](https://github.com/PennLINC/babs/compare/0.0.8...0.2.0)
+
+
+## Version 0.1.0
+
+### What's Changed
+
+#### Exciting New Features
+* Convert CLIs to subcommands by @tsalo in [#210](https://github.com/PennLINC/babs/pull/210)
+* Add a yml file for creating a mamba environment on an hpc by @mattcieslak in [#217](https://github.com/PennLINC/babs/pull/217)
+
+#### Bug Fixes
+* [FIX] Output RIA path not found error by @tientong98 in [#178](https://github.com/PennLINC/babs/pull/178)
+* Replace `pkg_resources` with `importlib.metadata` by @tientong98 in [#211](https://github.com/PennLINC/babs/pull/211)
+
+#### Other Changes
+* add backoff strategy for job polling by @asmacdo in [#165](https://github.com/PennLINC/babs/pull/165)
+* Introducing e2e slurm tests by @asmacdo in [#169](https://github.com/PennLINC/babs/pull/169)
+* [DOCS] Add examples of --list_sub_file/--list-sub-file by @tientong98 in [#181](https://github.com/PennLINC/babs/pull/181)
+* [DOCS] Add examples of --list_sub_file/--list-sub-file - Fixed rendering issues by @tientong98 in [#183](https://github.com/PennLINC/babs/pull/183)
+* update installation instructions with method to provide OSF credentials by @B-Sevchik in [#186](https://github.com/PennLINC/babs/pull/186)
+* Add participant selection flag to `babs-init` config yaml file by @tientong98 in [#187](https://github.com/PennLINC/babs/pull/187)
+* Support SLURM array jobs in `babs-submit` by @tientong98 in [#188](https://github.com/PennLINC/babs/pull/188)
+* Add a new docker build and fix CI tests by @mattcieslak in [#189](https://github.com/PennLINC/babs/pull/189)
+* Restyle with ruff by @mattcieslak in [#190](https://github.com/PennLINC/babs/pull/190)
+* Add back containerized slurm to the CI by @mattcieslak in [#191](https://github.com/PennLINC/babs/pull/191)
+* Fix shellcheck by @mattcieslak in [#198](https://github.com/PennLINC/babs/pull/198)
+... (5 more lines)
+~~~
+
+**Mediation** (Easy): Consolidate repeated sections within this document
+
+> Same content appears multiple times in one file. Merge into a single section and add internal cross-references.
+
+
+
+
+Cluster 7: [Moderate] `tests/e2e-slurm/container/config_simbids.yaml` lines 14-38 ↔ `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 17-45 (25 lines 68% of file)
+
+**Files involved:**
+- [`tests/e2e-slurm/container/config_simbids.yaml` (lines 14-38)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids.yaml#L14-L38)
+- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 17-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L17-L45)
+
+**Duplicated fragment:**
+~~~yaml
+: ""
+
+all_results_in_one_zip: true
+zip_foldernames:
+ fmriprep_anat: "24-1-1"
+
+singularity_args:
+ - --containall
+ - --writable-tmpfs
+
+cluster_resources:
+ interpreting_shell: "/bin/bash"
+
+script_preamble: |
+ PATH=/opt/conda/envs/babs/bin:$PATH
+
+job_compute_space: "/tmp"
+
+alert_log_messages:
+ stdout:
+ - "Excessive topologic defect encountered"
+ - "Cannot allocate memory"
+ - "mris_curvature_stats: Could not open file"
+ - "Numerical result out of range"
+ - "fMRIPrep failed"
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 8: [Moderate] `tests/test_check_setup.py` lines 60-83 ↔ `tests/test_check_setup.py` lines 29-52 (24 lines)
+
+**Files involved:**
+- [`tests/test_check_setup.py` (lines 60-83)](https://github.com/PennLINC/babs/blob/main/tests/test_check_setup.py#L60-L83)
+- [`tests/test_check_setup.py` (lines 29-52)](https://github.com/PennLINC/babs/blob/main/tests/test_check_setup.py#L29-L52)
+
+**Duplicated fragment:**
+~~~python
+(tmp_path_factory, babs_project_sessionlevel, monkeypatch):
+ """Test that the input_shasum is correctly checked."""
+
+ project_root = babs_project_sessionlevel
+
+ babs_proj = BABSCheckSetup(project_root)
+ # Make sure we have the dataset ID
+ babs_proj.wtf_key_info()
+
+ # Mock read_yaml to avoid creating lock files
+ def mock_read_yaml(path, use_filelock=False):
+ import yaml
+
+ with open(path) as file:
+ return yaml.safe_load(file)
+
+ monkeypatch.setattr('babs.check_setup.read_yaml', mock_read_yaml)
+
+ # Run check-setup without test job
+ babs_proj.babs_check_setup(submit_a_test_job=False)
+
+ # make a change to the input ria
+ add_commit_to_ria(
+ f'ria+file://{babs_proj.output_ria_path}#{babs_proj.analysis_dataset_id}'
+~~~
+
+**Mediation** (Moderate): Extract test fixture or parametrize
+
+> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern.
+
+
+
+
+Cluster 9: [Moderate] `tests/e2e-slurm/container/config_simbids_multiinput.yaml` lines 19-41 ↔ `tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml` lines 23-45 (23 lines 58% of file)
+
+**Files involved:**
+- [`tests/e2e-slurm/container/config_simbids_multiinput.yaml` (lines 19-41)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/config_simbids_multiinput.yaml#L19-L41)
+- [`tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml` (lines 23-45)](https://github.com/PennLINC/babs/blob/main/tests/e2e-slurm/container/simbids_fmriprep-24-1-1_anatonly.yaml#L23-L45)
+
+**Duplicated fragment:**
+~~~yaml
+all_results_in_one_zip: true
+zip_foldernames:
+ fmriprep_anat: "24-1-1"
+
+singularity_args:
+ - --containall
+ - --writable-tmpfs
+
+cluster_resources:
+ interpreting_shell: "/bin/bash"
+
+script_preamble: |
+ PATH=/opt/conda/envs/babs/bin:$PATH
+
+job_compute_space: "/tmp"
+
+alert_log_messages:
+ stdout:
+ - "Excessive topologic defect encountered"
+ - "Cannot allocate memory"
+ - "mris_curvature_stats: Could not open file"
+ - "Numerical result out of range"
+ - "fMRIPrep failed"
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 10: [Moderate] `tests/test_babs_workflow.py` lines 45-62 ↔ `tests/test_update_input_data.py` lines 126-142 (18 lines)
+
+**Files involved:**
+- [`tests/test_babs_workflow.py` (lines 45-62)](https://github.com/PennLINC/babs/blob/main/tests/test_babs_workflow.py#L45-L62)
+- [`tests/test_update_input_data.py` (lines 126-142)](https://github.com/PennLINC/babs/blob/main/tests/test_update_input_data.py#L126-L142)
+
+**Duplicated fragment:**
+~~~python
+)
+
+ # Preparation of env variable `TEMPLATEFLOW_HOME`:
+ os.environ['TEMPLATEFLOW_HOME'] = str(templateflow_home)
+ assert os.getenv('TEMPLATEFLOW_HOME')
+
+ # Get the cli of `babs init`:
+ project_base = tmp_path_factory.mktemp('project')
+ project_root = project_base / 'my_babs_project'
+ container_name = 'simbids-0-0-3'
+
+ # Use config_simbids.yaml instead of eg_fmriprep
+ config_simbids_path = get_config_simbids_path()
+ container_config = update_yaml_for_run(
+ project_base,
+ config_simbids_path.name,
+ {
+ 'BIDS': bids_data_singlesession
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 11: [Easy] `babs/cli.py` lines 309-326 ↔ `babs/cli.py` lines 228-244 (18 lines)
+
+**Files involved:**
+- [`babs/cli.py` (lines 309-326)](https://github.com/PennLINC/babs/blob/main/babs/cli.py#L309-L326)
+- [`babs/cli.py` (lines 228-244)](https://github.com/PennLINC/babs/blob/main/babs/cli.py#L228-L244)
+
+**Duplicated fragment:**
+~~~python
+,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ PathExists = partial(_path_exists, parser=parser)
+ parser.add_argument(
+ 'project_root',
+ metavar='PATH',
+ help=(
+ 'Absolute path to the root of BABS project. '
+ "For example, '/path/to/my_BABS_project/' "
+ '(default is current working directory).'
+ ),
+ nargs='?',
+ default=Path.cwd(),
+ type=PathExists,
+ )
+
+ group
+~~~
+
+**Mediation** (Easy): Extract `_base_project_parser(description)` helper
+
+> Three subcommand parsers (`_parse_check_setup`, `_parse_submit`, `_parse_status`) all create a parser with `ArgumentDefaultsHelpFormatter`, `PathExists`, and the identical `project_root` positional argument. Extract a `_base_project_parser(description)` function that returns a parser pre-configured with `project_root`, then each `_parse_*` function calls it and adds its own extra arguments. This covers both Clusters 11 and 12.
+
+```python
+def _base_project_parser(description):
+ parser = argparse.ArgumentParser(
+ description=description,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ PathExists = partial(_path_exists, parser=parser)
+ parser.add_argument(
+ 'project_root', metavar='PATH',
+ help=('Absolute path to the root of BABS project. '
+ "For example, '/path/to/my_BABS_project/' "
+ '(default is current working directory).'),
+ nargs='?', default=Path.cwd(), type=PathExists,
+ )
+ return parser
+```
+
+
+
+
+Cluster 13: [Moderate] `tests/conftest.py` lines 424-440 ↔ `tests/test_babs_workflow.py` lines 45-142 (17 lines)
+
+**Files involved:**
+- [`tests/conftest.py` (lines 424-440)](https://github.com/PennLINC/babs/blob/main/tests/conftest.py#L424-L440)
+- [`tests/test_babs_workflow.py` (lines 45-142)](https://github.com/PennLINC/babs/blob/main/tests/test_babs_workflow.py#L45-L142)
+
+**Duplicated fragment:**
+~~~python
+, '.datalad/config'))
+
+ # Preparation of env variable `TEMPLATEFLOW_HOME`:
+ os.environ['TEMPLATEFLOW_HOME'] = str(templateflow_home)
+ assert os.getenv('TEMPLATEFLOW_HOME')
+
+ # Get the cli of `babs init`:
+ project_base = tmp_path_factory.mktemp('project')
+ project_root = project_base / 'my_babs_project'
+ container_name = 'simbids-0-0-3'
+
+ # Use config_simbids.yaml instead of eg_fmriprep
+ config_simbids_path = get_config_simbids_path()
+ container_config = update_yaml_for_run(
+ project_base,
+ config_simbids_path.name,
+ {'BIDS': bids_data
+~~~
+
+**Mediation** (Moderate): Extract shared test fixture to conftest.py
+
+> Duplicated test code across files. Move common setup into `conftest.py` as a shared fixture, or into a test utilities module.
+
+
+
+
+Cluster 14: [Moderate] `tests/test_input_datasets.py` lines 250-263 ↔ `tests/test_input_datasets.py` lines 108-121 (14 lines)
+
+**Files involved:**
+- [`tests/test_input_datasets.py` (lines 250-263)](https://github.com/PennLINC/babs/blob/main/tests/test_input_datasets.py#L250-L263)
+- [`tests/test_input_datasets.py` (lines 108-121)](https://github.com/PennLINC/babs/blob/main/tests/test_input_datasets.py#L108-L121)
+
+**Duplicated fragment:**
+~~~python
+if session_type == 'nosessions':
+ origin_url = fmriprep_noses_derivative_files_zipped_at_subject
+ elif session_type == 'sessions':
+ if processing_level == 'session':
+ origin_url = fmriprep_multises_derivative_files_zipped_at_session
+ else:
+ origin_url = fmriprep_multises_derivative_files_zipped_at_subject
+ else:
+ raise ValueError(f'Invalid session type: {session_type}')
+
+ clone_path = tmp_path_factory.mktemp('cloned_input_dataset')
+ dlapi.clone(origin_url, path=clone_path)
+
+ input_dataset
+~~~
+
+**Mediation** (Moderate): Extract test fixture or parametrize
+
+> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern.
+
+
+
+
+Cluster 15: [Moderate] `tests/test_utils.py` lines 454-465 ↔ `tests/test_utils.py` lines 309-320 (12 lines)
+
+**Files involved:**
+- [`tests/test_utils.py` (lines 454-465)](https://github.com/PennLINC/babs/blob/main/tests/test_utils.py#L454-L465)
+- [`tests/test_utils.py` (lines 309-320)](https://github.com/PennLINC/babs/blob/main/tests/test_utils.py#L309-L320)
+
+**Duplicated fragment:**
+~~~python
+, 'first_run'],
+ 'has_results': [False, False, False, True],
+ # Fields for tracking:
+ 'needs_resubmit': [False, False, False, False],
+ 'is_failed': [pd.NA, pd.NA, pd.NA, False],
+ 'log_filename': [pd.NA, pd.NA, pd.NA, 'test_array_job.log'],
+ 'last_line_stdout_file': [pd.NA, pd.NA, pd.NA, 'SUCCESS'],
+ 'alert_message': [pd.NA, pd.NA, pd.NA, pd.NA],
+ }
+ )
+
+ new_status_df
+~~~
+
+**Mediation** (Moderate): Extract test fixture or parametrize
+
+> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern.
+
+
+
+
+Cluster 16: [Moderate] `tests/test_interaction.py` lines 77-87 ↔ `tests/test_interaction.py` lines 56-67 (11 lines)
+
+**Files involved:**
+- [`tests/test_interaction.py` (lines 77-87)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L77-L87)
+- [`tests/test_interaction.py` (lines 56-67)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L56-L67)
+
+**Duplicated fragment:**
+~~~python
+],
+ 'time_used': ['0:01'],
+ 'time_limit': ['5-00:00:00'],
+ 'nodes': [1],
+ 'cpus': [1],
+ 'partition': ['normal'],
+ 'name': ['test_array_job'],
+ }
+ )
+ monkeypatch.setattr(babs_proj, 'get_currently_running_jobs_df', lambda: running_df)
+ monkeypatch
+~~~
+
+**Mediation** (Moderate): Extract test fixture or parametrize
+
+> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern.
+
+
+
+
+Cluster 17: [Easy] `tests/test_interaction.py` lines 106-115 ↔ `tests/test_interaction.py` lines 54-63 (10 lines)
+
+**Files involved:**
+- [`tests/test_interaction.py` (lines 106-115)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L106-L115)
+- [`tests/test_interaction.py` (lines 54-63)](https://github.com/PennLINC/babs/blob/main/tests/test_interaction.py#L54-L63)
+
+**Duplicated fragment:**
+~~~python
+],
+ 'task_id': [1],
+ 'state': ['R'],
+ 'time_used': ['0:01'],
+ 'time_limit': ['5-00:00:00'],
+ 'nodes': [1],
+ 'cpus': [1],
+ 'partition': ['normal'],
+ 'name': ['test_array_job'],
+ 'sub_id'
+~~~
+
+**Mediation** (Easy): Extract test fixture or parametrize
+
+> Duplicated test setup/assertions within one test file. Use `@pytest.fixture`, `@pytest.mark.parametrize`, or a helper function to share the common pattern.
+
+
+
+
+Cluster 18: [Easy] `babs/check_setup.py` lines 343-351 ↔ `babs/check_setup.py` lines 293-302 (9 lines)
+
+**Files involved:**
+- [`babs/check_setup.py` (lines 343-351)](https://github.com/PennLINC/babs/blob/main/babs/check_setup.py#L343-L351)
+- [`babs/check_setup.py` (lines 293-302)](https://github.com/PennLINC/babs/blob/main/babs/check_setup.py#L293-L302)
+
+**Duplicated fragment:**
+~~~python
+sleeptime = 0
+ while not new_job_status.empty:
+ sleeptime += 1
+ time.sleep(sleeptime)
+ job_status = new_job_status.copy()
+ new_job_status = request_all_job_status(self.queue, job_id)
+
+ if not job_status.shape[0] == 1:
+ raise Exception(f'Expected 1 job, got {job_status.shape[0]}'
+~~~
+
+**Mediation** (Easy): Extract `_wait_for_job_completion(queue, job_id)` helper
+
+> The "submit job → poll until done → validate single row" pattern at lines 292-302 and 340-351 is identical. Extract a helper like `_wait_for_job_completion(queue, job_id)` that returns the final `job_status` DataFrame. Both the pipeline-step loop and single-container branch would call it.
+
+