8Regression tests for :mod:`oct.mcp.redactor` — five-step output
13- Verify secret-line filtering (T-03) and path anonymisation (T-07).
14- Verify ANSI stripping, truncation, and git URL redaction.
15- Verify custom redaction patterns.
22 L3 — assertion details
27- Tests operate on in-memory strings; no files are written.
30from __future__
import annotations
33from pathlib
import Path
50 r =
RedactorResult(text=
"hi", redactions_applied=0, truncated=
False, original_size_bytes=2)
52 assert r.redactions_applied == 0
53 assert r.truncated
is False
62 r =
Redactor(redaction_patterns=[
"MYSECRET"])
63 result = r.apply_all(
"output MYSECRET value", tmp_path)
64 assert "MYSECRET" not in result.text
65 assert "[REDACTED]" in result.text
66 assert result.redactions_applied >= 1
69 r =
Redactor(redaction_patterns=[{
"pattern":
r"\bJWT_\w+",
"replacement":
"[JWT]"}])
70 result = r.apply_all(
"token JWT_xyz123 used", tmp_path)
71 assert "[JWT]" in result.text
75 r =
Redactor(redaction_patterns=[
"[invalid(regex"])
76 result = r.apply_all(
"some text", tmp_path)
77 assert result.text ==
"some text"
81 result = r.apply_all(
"clean output", tmp_path)
82 assert result.text ==
"clean output"
92 result = r.apply_all(
"API_KEY=abc123secret", tmp_path)
93 assert "abc123secret" not in result.text
94 assert "[REDACTED]" in result.text
98 result = r.apply_all(
"password=hunter2", tmp_path)
99 assert "hunter2" not in result.text
103 result = r.apply_all(
"token=ghp_xxxxxxxxxxxx", tmp_path)
104 assert "ghp_xxxxxxxxxxxx" not in result.text
108 result = r.apply_all(
"secret: mysecretvalue", tmp_path)
109 assert "mysecretvalue" not in result.text
113 result = r.apply_all(
"filename=output.py", tmp_path)
114 assert "output.py" in result.text
118 text =
"result=OK\napi_key=secret123\nstatus=pass"
119 result = r.apply_all(text, tmp_path)
120 assert "OK" in result.text
121 assert "pass" in result.text
122 assert "secret123" not in result.text
125 """T-03: API_KEY in output must be redacted before reaching client."""
127 output =
"Running lint...\nAPI_KEY=sk-abc123\nLint passed."
128 result = r.apply_all(output, tmp_path)
129 assert "sk-abc123" not in result.text
130 assert result.redactions_applied >= 1
140 result = r.apply_all(
"remote: https://user:token@github.com/repo", tmp_path)
141 assert "user:token" not in result.text
142 assert "***@" in result.text
152 home = str(Path.home())
153 result = r.apply_all(f
"config at {home}/something", tmp_path)
154 assert "[HOME]" in result.text
155 assert home
not in result.text
159 result = r.apply_all(f
"file: {tmp_path}/src/main.py", tmp_path)
160 assert "[PROJECT]" in result.text
161 assert str(tmp_path)
not in result.text
164 """T-07: Absolute project path must not reach the client."""
166 output = f
"Error in {tmp_path}/oct/mcp/config.py at line 42"
167 result = r.apply_all(output, tmp_path)
168 assert str(tmp_path)
not in result.text
169 assert "[PROJECT]" in result.text
172 """T-07: Home directory path must be anonymised."""
174 home = str(Path.home())
175 output = f
"Audit log at {home}/.oct-mcp/audit.log"
176 result = r.apply_all(output, tmp_path)
177 assert home
not in result.text
187 result = r.apply_all(
"\x1b[32mOK\x1b[0m", tmp_path)
188 assert "\x1b" not in result.text
189 assert "OK" in result.text
193 result = r.apply_all(
"\x1b[1mBold\x1b[22m", tmp_path)
194 assert "\x1b" not in result.text
195 assert "Bold" in result.text
199 result = r.apply_all(
"line\x1b[Aclear", tmp_path)
200 assert "\x1b" not in result.text
204 result = r.apply_all(
"No ANSI codes here", tmp_path)
205 assert result.text ==
"No ANSI codes here"
206 assert result.redactions_applied == 0
216 result = r.apply_all(
"short output", tmp_path)
217 assert result.truncated
is False
222 result = r.apply_all(big, tmp_path)
223 assert result.truncated
is True
224 assert "TRUNCATED" in result.text
228 result = r.apply_all(
"a" * 200, tmp_path)
229 assert "[OUTPUT TRUNCATED AT 50 BYTES]" in result.text
234 result = r.apply_all(text, tmp_path)
235 assert result.truncated
is False
240 result = r.apply_all(text, tmp_path)
241 assert result.original_size_bytes == len(text.encode(
"utf-8"))
251 result = r.apply_all(
"", tmp_path)
252 assert result.text ==
""
256 result = r.apply_all(
"output", tmp_path)
257 assert isinstance(result, RedactorResult)
260 r =
Redactor(redaction_patterns=[
"SECRET"])
261 text =
"API_KEY=abc\nSECRET\nSECRET"
262 result = r.apply_all(text, tmp_path)
264 assert result.redactions_applied >= 3
test_returns_redactor_result(self, Path tmp_path)
test_never_raises_on_empty_text(self, Path tmp_path)
test_redaction_count_accumulates(self, Path tmp_path)
test_fields_present(self)
test_invalid_pattern_skipped(self, Path tmp_path)
test_no_patterns_no_change(self, Path tmp_path)
test_custom_pattern_string_redacted(self, Path tmp_path)
test_custom_pattern_dict_with_replacement(self, Path tmp_path)
test_colon_separator_redacted(self, Path tmp_path)
test_token_assignment_redacted(self, Path tmp_path)
test_non_secret_key_preserved(self, Path tmp_path)
test_multiline_only_secret_line_redacted(self, Path tmp_path)
test_t03_secret_exfiltration_blocked(self, Path tmp_path)
test_password_assignment_redacted(self, Path tmp_path)
test_api_key_assignment_redacted(self, Path tmp_path)
test_http_credentials_redacted(self, Path tmp_path)
test_t07_home_path_not_disclosed(self, Path tmp_path)
test_project_root_replaced(self, Path tmp_path)
test_home_path_replaced(self, Path tmp_path)
test_t07_info_disclosure_project_root(self, Path tmp_path)
test_cursor_movement_removed(self, Path tmp_path)
test_bold_codes_removed(self, Path tmp_path)
test_colour_codes_removed(self, Path tmp_path)
test_clean_text_unchanged(self, Path tmp_path)
test_original_size_bytes_recorded(self, Path tmp_path)
test_truncation_marker_present(self, Path tmp_path)
test_exactly_at_limit_not_truncated(self, Path tmp_path)
test_output_under_limit_not_truncated(self, Path tmp_path)
test_output_over_limit_truncated(self, Path tmp_path)
Redactor make_redactor(**kwargs)