11# Copyright (c) Microsoft Corporation.
22# Licensed under the MIT license.
33
4- """Tests that exercise the full attack class wiring without mocking manager classes.
5-
6- These tests catch kwarg mismatches between IndividualPromptAttack/ProgressiveMultiPromptAttack
7- and MultiPromptAttack.__init__(), and template compatibility issues in _update_ids().
4+ """Integration tests for GCG attack-class wiring.
5+
6+ These tests construct real ``IndividualPromptAttack`` / ``ProgressiveMultiPromptAttack``
7+ instances and exercise their wiring all the way down through
8+ ``GCGAttackPrompt._update_ids``, which calls ``tokenizer.apply_chat_template`` and
9+ walks character positions via ``char_to_token``. They live here (not under
10+ ``tests/unit/``) because they require a real HuggingFace tokenizer (gpt2) to
11+ back the chat-template pipeline — mocking that out would defeat the test's
12+ purpose, which is to catch kwarg-mismatch and template-compatibility bugs that
13+ mocked tests miss.
14+
15+ Network dependency: the gpt2 tokenizer is fetched from HuggingFace on first
16+ use (and then cached). This is why these tests run only in the integration tier
17+ (``make integration-test``), not in the PR-time unit-test matrix.
18+
19+ Requires: torch, transformers (GCG optional deps).
20+ Skipped via importorskip when deps are not installed.
821"""
922
23+ from __future__ import annotations
24+
25+ from typing import TYPE_CHECKING
1026from unittest .mock import MagicMock , patch
1127
1228import pytest
2238 reason = "GCG optional dependencies not installed" ,
2339)
2440
41+ generator_mod = pytest .importorskip (
42+ "pyrit.auxiliary_attacks.gcg.generator" ,
43+ reason = "GCG optional dependencies (torch, transformers, etc.) not installed" ,
44+ )
45+
46+ from pyrit .auxiliary_attacks .gcg .config import ( # noqa: E402
47+ GCGAlgorithmConfig ,
48+ GCGModelConfig ,
49+ GCGOutputConfig ,
50+ GCGStrategyConfig ,
51+ )
52+
53+ if TYPE_CHECKING :
54+ from pathlib import Path
55+
2556IndividualPromptAttack = attack_manager_mod .IndividualPromptAttack
2657ProgressiveMultiPromptAttack = attack_manager_mod .ProgressiveMultiPromptAttack
2758MultiPromptAttack = attack_manager_mod .MultiPromptAttack
2859GCGAttackPrompt = gcg_attack_mod .GCGAttackPrompt
2960GCGPromptManager = gcg_attack_mod .GCGPromptManager
3061GCGMultiPromptAttack = gcg_attack_mod .GCGMultiPromptAttack
62+ GCGGenerator = generator_mod .GCGGenerator
63+ GCGContext = generator_mod .GCGContext
3164
3265MANAGERS = {
3366 "AP" : GCGAttackPrompt ,
3467 "PM" : GCGPromptManager ,
3568 "MPA" : GCGMultiPromptAttack ,
3669}
3770
71+ _LLAMA_2 = "meta-llama/Llama-2-7b-chat-hf"
72+
3873
39- def _make_mock_worker () -> MagicMock :
40- """Create a mock worker whose tokenizer can stand in for a real chat tokenizer.
74+ def _make_mock_worker_with_real_tokenizer () -> MagicMock :
75+ """Worker mock backed by a real gpt2 tokenizer.
4176
4277 The wiring tests construct real ``GCGAttackPrompt`` instances which call
4378 ``tokenizer.apply_chat_template`` and then walk character positions in the
4479 rendered prompt. We need a real string + a tokenizer that can answer
45- ``char_to_token`` queries on it, so we back the mock with a real
46- distilgpt2 tokenizer (the smallest available transformers tokenizer that
47- ships with all the methods we touch ).
80+ ``char_to_token`` queries on it, so we back the mock with the smallest
81+ workable real HF tokenizer (gpt2) plus an explicit llama-2-style chat
82+ template (gpt2 ships without one ).
4883 """
4984 from transformers import AutoTokenizer
5085
@@ -67,9 +102,9 @@ def _make_mock_worker() -> MagicMock:
67102
68103
69104class TestAttackClassWiring :
70- """Tests that verify attack classes can be constructed with real manager classes.
105+ """Verify attack classes can be constructed and run with real manager classes.
71106
72- These catch kwarg mismatches that mocked tests miss.
107+ Catches kwarg mismatches that mocked tests miss.
73108 """
74109
75110 def test_individual_attack_creates_mpa_without_error (self ) -> None :
@@ -78,7 +113,7 @@ def test_individual_attack_creates_mpa_without_error(self) -> None:
78113 This catches the mpa_kwargs bug where dead kwargs (deterministic, lr, etc.)
79114 were passed to MultiPromptAttack.__init__() which didn't accept them.
80115 """
81- worker = _make_mock_worker ()
116+ worker = _make_mock_worker_with_real_tokenizer ()
82117
83118 # Create IndividualPromptAttack with the real GCG manager classes
84119 attack = IndividualPromptAttack (
@@ -114,7 +149,7 @@ def test_individual_attack_creates_mpa_without_error(self) -> None:
114149
115150 def test_progressive_attack_creates_mpa_without_error (self ) -> None :
116151 """ProgressiveMultiPromptAttack.run() should create MultiPromptAttack without TypeError."""
117- worker = _make_mock_worker ()
152+ worker = _make_mock_worker_with_real_tokenizer ()
118153
119154 attack = ProgressiveMultiPromptAttack (
120155 goals = ["test goal" ],
@@ -145,3 +180,54 @@ def test_progressive_attack_creates_mpa_without_error(self) -> None:
145180 verbose = False ,
146181 filter_cand = True ,
147182 )
183+
184+
185+ class TestCreateAttackWiring :
186+ """Construct real attack classes via :meth:`GCGGenerator._create_attack` to catch kwarg mismatches."""
187+
188+ def test_transfer_false_returns_individual (self , tmp_path : Path ) -> None :
189+ gen = GCGGenerator (
190+ models = [GCGModelConfig (name = _LLAMA_2 )],
191+ algorithm = GCGAlgorithmConfig (n_steps = 5 , batch_size = 64 , control_init = "! ! !" ),
192+ output = GCGOutputConfig (result_prefix = str (tmp_path / "gcg" )),
193+ )
194+ worker = _make_mock_worker_with_real_tokenizer ()
195+ context = GCGContext (goals = ["g" ], targets = ["t" ])
196+ params = gen ._to_attack_params (context = context )
197+
198+ attack = gen ._create_attack (
199+ params = params ,
200+ managers = MANAGERS ,
201+ train_goals = ["g" ],
202+ train_targets = ["t" ],
203+ test_goals = [],
204+ test_targets = [],
205+ workers = [worker ],
206+ test_workers = [],
207+ logfile_path = str (tmp_path / "log.json" ),
208+ )
209+ assert isinstance (attack , IndividualPromptAttack )
210+
211+ def test_transfer_true_returns_progressive (self , tmp_path : Path ) -> None :
212+ gen = GCGGenerator (
213+ models = [GCGModelConfig (name = _LLAMA_2 )],
214+ algorithm = GCGAlgorithmConfig (n_steps = 5 , batch_size = 64 , control_init = "! ! !" ),
215+ strategy = GCGStrategyConfig (transfer = True , progressive_goals = True , progressive_models = True ),
216+ output = GCGOutputConfig (result_prefix = str (tmp_path / "gcg" )),
217+ )
218+ worker = _make_mock_worker_with_real_tokenizer ()
219+ context = GCGContext (goals = ["g" ], targets = ["t" ])
220+ params = gen ._to_attack_params (context = context )
221+
222+ attack = gen ._create_attack (
223+ params = params ,
224+ managers = MANAGERS ,
225+ train_goals = ["g" ],
226+ train_targets = ["t" ],
227+ test_goals = [],
228+ test_targets = [],
229+ workers = [worker ],
230+ test_workers = [],
231+ logfile_path = str (tmp_path / "log.json" ),
232+ )
233+ assert isinstance (attack , ProgressiveMultiPromptAttack )
0 commit comments