panopticon-cli 1.1.0__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/PKG-INFO +2 -1
  2. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/README.md +1 -0
  3. panopticon_cli-1.1.2/panopticon/cli.py +267 -0
  4. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/PKG-INFO +2 -1
  5. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/pyproject.toml +5 -1
  6. panopticon_cli-1.1.2/tests/test_cli.py +63 -0
  7. panopticon_cli-1.1.0/panopticon/cli.py +0 -197
  8. panopticon_cli-1.1.0/tests/test_cli.py +0 -15
  9. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/LICENSE +0 -0
  10. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon/__init__.py +0 -0
  11. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon/memory.py +0 -0
  12. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon/observer.py +0 -0
  13. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon/policies.py +0 -0
  14. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon/sentinel.py +0 -0
  15. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/SOURCES.txt +0 -0
  16. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/dependency_links.txt +0 -0
  17. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/entry_points.txt +0 -0
  18. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/requires.txt +0 -0
  19. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/top_level.txt +0 -0
  20. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/setup.cfg +0 -0
  21. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/tests/test_memory.py +0 -0
  22. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/tests/test_observer.py +0 -0
  23. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/tests/test_policies.py +0 -0
  24. {panopticon_cli-1.1.0 → panopticon_cli-1.1.2}/tests/test_sentinel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -34,6 +34,7 @@ Dynamic: license-file
34
34
 
35
35
  [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
36
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![CI](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
37
38
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
38
39
 
39
40
  Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![CI](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
7
8
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
8
9
 
9
10
  Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
@@ -0,0 +1,267 @@
1
+ import subprocess
2
+ import threading
3
+ import sys
4
+ import time
5
+ import argparse
6
+ import re
7
+ import os
8
+ import signal
9
+ import codecs
10
+ import atexit
11
+ from typing import List
12
+ from .observer import PanopticonObserver, InterventionException
13
+ from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
14
+
15
+
16
+ def strip_ansi(text: str) -> str:
17
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
18
+ return ansi_escape.sub("", text)
19
+
20
+
21
+ class CLIWrapper:
22
+ def __init__(self, command: List[str]):
23
+ self.command = command
24
+ target_agent = self.command[0] if self.command else "unknown"
25
+
26
+ self.observer = PanopticonObserver(
27
+ policies=[
28
+ BlacklistPolicy(forbidden_patterns=["rm -rf /", "DROP TABLE"]),
29
+ AntiLoopPolicy(window=3, threshold=0.85),
30
+ AdversarialLogicCheck(target_agent=target_agent),
31
+ ]
32
+ )
33
+
34
+ self.process = None
35
+ self.buffer = []
36
+ self.running = False
37
+ self.lock = threading.Lock()
38
+ self.threads = []
39
+ self.stdin_reader = None
40
+
41
+ def run(self):
42
+ self.running = True
43
+ env = os.environ.copy()
44
+ env["PYTHONUNBUFFERED"] = "1"
45
+ env["FORCE_COLOR"] = "1"
46
+ env["CLICOLOR_FORCE"] = "1"
47
+
48
+ print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
49
+ print(
50
+ f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), "
51
+ f"AdversarialLogic"
52
+ )
53
+
54
+ # Flaw 2 Fix: Removing text=True to read raw binary bytes
55
+ self.process = subprocess.Popen(
56
+ self.command,
57
+ stdout=subprocess.PIPE,
58
+ stderr=subprocess.STDOUT,
59
+ stdin=subprocess.PIPE,
60
+ bufsize=0,
61
+ env=env,
62
+ )
63
+
64
+ t_out = threading.Thread(target=self._read_stdout, daemon=True)
65
+ t_out.start()
66
+ self.threads.append(t_out)
67
+
68
+ t_eval = threading.Thread(target=self._eval_loop, daemon=True)
69
+ t_eval.start()
70
+ self.threads.append(t_eval)
71
+
72
+ t_in = threading.Thread(target=self._read_stdin, daemon=True)
73
+ t_in.start()
74
+ self.threads.append(t_in)
75
+
76
+ # Register cleanup on exit
77
+ atexit.register(self._cleanup)
78
+
79
+ try:
80
+ self.process.wait()
81
+ except KeyboardInterrupt:
82
+ pass
83
+ finally:
84
+ self.running = False
85
+ self._cleanup()
86
+
87
+ def _cleanup(self):
88
+ """Cleanly shutdown all threads and resources."""
89
+ self.running = False
90
+
91
+ # Close subprocess pipes FIRST before thread cleanup
92
+ if self.process:
93
+ try:
94
+ if self.process.stdin and not self.process.stdin.closed:
95
+ self.process.stdin.close()
96
+ except Exception:
97
+ pass
98
+
99
+ try:
100
+ if self.process.stdout and not self.process.stdout.closed:
101
+ self.process.stdout.close()
102
+ except Exception:
103
+ pass
104
+
105
+ try:
106
+ if self.process.poll() is None:
107
+ self.process.terminate()
108
+ try:
109
+ self.process.wait(timeout=2)
110
+ except subprocess.TimeoutExpired:
111
+ self.process.kill()
112
+ except Exception:
113
+ pass
114
+
115
+ # Wait for threads with timeout
116
+ for thread in self.threads:
117
+ if thread.is_alive():
118
+ thread.join(timeout=1)
119
+
120
+ def _read_stdout(self):
121
+ # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
122
+ decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
123
+ try:
124
+ while self.running and self.process and self.process.poll() is None:
125
+ try:
126
+ byte_chunk = self.process.stdout.read(1)
127
+ if not byte_chunk:
128
+ break
129
+
130
+ # Mirror raw bytes to actual terminal seamlessly
131
+ sys.stdout.buffer.write(byte_chunk)
132
+ sys.stdout.buffer.flush()
133
+
134
+ char_str = decoder.decode(byte_chunk)
135
+ if char_str:
136
+ with self.lock:
137
+ self.buffer.append(char_str)
138
+ except (OSError, ValueError, BrokenPipeError):
139
+ break
140
+ except Exception:
141
+ pass
142
+ finally:
143
+ try:
144
+ if (
145
+ self.process
146
+ and self.process.stdout
147
+ and (not self.process.stdout.closed)
148
+ ):
149
+ self.process.stdout.close()
150
+ except Exception:
151
+ pass
152
+
153
+ def _read_stdin(self):
154
+ try:
155
+ while self.running and self.process and self.process.poll() is None:
156
+ try:
157
+ data = sys.stdin.buffer.read1(1024)
158
+ if not data:
159
+ break
160
+ if self.process.stdin and not self.process.stdin.closed:
161
+ self.process.stdin.write(data)
162
+ self.process.stdin.flush()
163
+ except (OSError, ValueError, BrokenPipeError):
164
+ break
165
+ except Exception:
166
+ pass
167
+ finally:
168
+ try:
169
+ if (
170
+ self.process
171
+ and self.process.stdin
172
+ and (not self.process.stdin.closed)
173
+ ):
174
+ self.process.stdin.close()
175
+ except Exception:
176
+ pass
177
+
178
+ def _eval_loop(self):
179
+ while self.running and self.process and self.process.poll() is None:
180
+ time.sleep(15)
181
+
182
+ with self.lock:
183
+ if not self.buffer:
184
+ continue
185
+ full_text = "".join(self.buffer)
186
+
187
+ # Flaw 1 Fix: Sliding window overlap to prevent bridging blindness
188
+ overlap = full_text[-100:] if len(full_text) > 100 else ""
189
+ self.buffer.clear()
190
+ if overlap:
191
+ self.buffer.append(overlap)
192
+
193
+ clean_text = strip_ansi(full_text)
194
+ self.observer.log_action(
195
+ "CLI_Agent", clean_text, "cli_execution", len(clean_text) // 4
196
+ )
197
+
198
+ try:
199
+ self.observer._evaluate_state("CLI_Agent")
200
+ except InterventionException as e:
201
+ print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
202
+ print(f"[REASON]: {e.args[0]}")
203
+ print(
204
+ f"[LIVE INJECTION]: Interrupting agent and injecting "
205
+ f"correction...\n"
206
+ )
207
+
208
+ # Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
209
+ try:
210
+ if sys.platform != "win32":
211
+ self.process.send_signal(signal.SIGINT)
212
+ # Give the agent a split second to catch the interrupt
213
+ time.sleep(0.5)
214
+ except Exception:
215
+ pass
216
+
217
+ try:
218
+ # Write the injection payload as binary
219
+ if self.process.stdin and not self.process.stdin.closed:
220
+ payload = (e.course_correction + "\n").encode("utf-8")
221
+ self.process.stdin.write(payload)
222
+ self.process.stdin.flush()
223
+ except Exception as ex:
224
+ print(
225
+ f"[ERROR] Live injection failed or agent deadlocked: "
226
+ f"{ex}. Hard terminating."
227
+ )
228
+ self.process.terminate()
229
+ self.running = False
230
+ break
231
+
232
+
233
+ def main():
234
+ try:
235
+ from dotenv import load_dotenv
236
+
237
+ load_dotenv()
238
+ except ImportError:
239
+ pass
240
+
241
+ parser = argparse.ArgumentParser(
242
+ description="Panopticon: Production-Grade Immune System for AI CLIs"
243
+ )
244
+ parser.add_argument(
245
+ "command", nargs=argparse.REMAINDER, help="The command to run, e.g., 'claude'"
246
+ )
247
+ args = parser.parse_args()
248
+
249
+ if not args.command:
250
+ print("Usage: panopticon [command]")
251
+ sys.exit(1)
252
+
253
+ wrapper = CLIWrapper(args.command)
254
+ try:
255
+ wrapper.run()
256
+ except KeyboardInterrupt:
257
+ print("\n[PANOPTICON] Interrupted by user. Cleaning up...")
258
+ wrapper._cleanup()
259
+ sys.exit(0)
260
+ except Exception as e:
261
+ print(f"[PANOPTICON ERROR] {e}")
262
+ wrapper._cleanup()
263
+ sys.exit(1)
264
+
265
+
266
+ if __name__ == "__main__":
267
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -34,6 +34,7 @@ Dynamic: license-file
34
34
 
35
35
  [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
36
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![CI](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
37
38
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
38
39
 
39
40
  Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "panopticon-cli"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "The Cognitive Immune System for Autonomous CLI Agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -41,3 +41,7 @@ dev = [
41
41
 
42
42
  [project.scripts]
43
43
  panopticon = "panopticon.cli:main"
44
+
45
+ [tool.black]
46
+ target_version = ["py39", "py310", "py311", "py312"]
47
+ line_length = 88
@@ -0,0 +1,63 @@
1
+ import io
2
+ import sys
3
+
4
+ from panopticon.cli import CLIWrapper, strip_ansi
5
+
6
+
7
+ class DummyStdinStream:
8
+ def __init__(self, data: bytes):
9
+ self.buffer = io.BytesIO(data)
10
+
11
+
12
+ class DummyProcessStdin:
13
+ def __init__(self):
14
+ self.written = bytearray()
15
+ self.closed = False
16
+
17
+ def write(self, data: bytes):
18
+ self.written.extend(data)
19
+
20
+ def flush(self):
21
+ pass
22
+
23
+ def close(self):
24
+ self.closed = True
25
+
26
+
27
+ class DummyProcess:
28
+ def __init__(self):
29
+ self.stdin = DummyProcessStdin()
30
+ self.stdout = io.BytesIO()
31
+
32
+ def poll(self):
33
+ return None
34
+
35
+
36
+ def test_strip_ansi():
37
+ # Basic red color text
38
+ colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
39
+ assert strip_ansi(colored_text) == "Fatal Error: File not found"
40
+
41
+ # Complex formatting (Bold + Green)
42
+ complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
43
+ assert strip_ansi(complex_text) == "Success"
44
+
45
+ # Loading spinner edge case
46
+ spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
47
+ assert "Loading..." in strip_ansi(spinner_text)
48
+
49
+
50
+ def test_read_stdin_forwards_input():
51
+ wrapper = CLIWrapper(["dummy"])
52
+ wrapper.running = True
53
+ wrapper.process = DummyProcess()
54
+
55
+ original_stdin = sys.stdin
56
+ try:
57
+ sys.stdin = DummyStdinStream(b"hello world\n")
58
+ wrapper._read_stdin()
59
+ finally:
60
+ sys.stdin = original_stdin
61
+
62
+ assert wrapper.process.stdin.written == b"hello world\n"
63
+ assert wrapper.process.stdin.closed is True
@@ -1,197 +0,0 @@
1
- import subprocess
2
- import threading
3
- import sys
4
- import time
5
- import argparse
6
- import re
7
- import os
8
- import signal
9
- import codecs
10
- from typing import List
11
- from .observer import PanopticonObserver, InterventionException
12
- from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
13
-
14
-
15
- def strip_ansi(text: str) -> str:
16
- ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
17
- return ansi_escape.sub("", text)
18
-
19
-
20
- class CLIWrapper:
21
- def __init__(self, command: List[str]):
22
- self.command = command
23
- target_agent = self.command[0] if self.command else "unknown"
24
-
25
- self.observer = PanopticonObserver(
26
- policies=[
27
- BlacklistPolicy(forbidden_patterns=["rm -rf /", "DROP TABLE"]),
28
- AntiLoopPolicy(window=3, threshold=0.85),
29
- AdversarialLogicCheck(target_agent=target_agent),
30
- ]
31
- )
32
-
33
- self.process = None
34
- self.buffer = []
35
- self.running = False
36
- self.lock = threading.Lock()
37
-
38
- def run(self):
39
- self.running = True
40
- env = os.environ.copy()
41
- env["PYTHONUNBUFFERED"] = "1"
42
- env["FORCE_COLOR"] = "1"
43
- env["CLICOLOR_FORCE"] = "1"
44
-
45
- print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
46
- print(
47
- f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), AdversarialLogic"
48
- )
49
-
50
- # Flaw 2 Fix: Removing text=True to read raw binary bytes
51
- self.process = subprocess.Popen(
52
- self.command,
53
- stdout=subprocess.PIPE,
54
- stderr=subprocess.STDOUT,
55
- stdin=subprocess.PIPE,
56
- bufsize=0,
57
- env=env,
58
- )
59
-
60
- t_out = threading.Thread(target=self._read_stdout)
61
- t_out.daemon = True
62
- t_out.start()
63
-
64
- t_in = threading.Thread(target=self._forward_stdin)
65
- t_in.daemon = True
66
- t_in.start()
67
-
68
- t_eval = threading.Thread(target=self._eval_loop)
69
- t_eval.daemon = True
70
- t_eval.start()
71
-
72
- self.process.wait()
73
- self.running = False
74
-
75
- def _forward_stdin(self):
76
- import select
77
- try:
78
- while self.running and self.process.poll() is None:
79
- # Non-blocking check: only read if data is available
80
- if sys.platform == "win32":
81
- # Windows doesn't support select on stdin, so use a small sleep
82
- time.sleep(0.1)
83
- else:
84
- # Unix/Linux: use select to check if stdin is readable
85
- ready, _, _ = select.select([sys.stdin], [], [], 0.5)
86
- if not ready:
87
- continue
88
-
89
- try:
90
- line = sys.stdin.buffer.readline()
91
- if not line: # EOF reached
92
- break
93
- self.process.stdin.write(line)
94
- self.process.stdin.flush()
95
- except (EOFError, OSError):
96
- break
97
- except Exception:
98
- pass
99
-
100
- def _read_stdout(self):
101
- # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
102
- decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
103
- try:
104
- while self.running and self.process.poll() is None:
105
- byte_chunk = self.process.stdout.read(1)
106
- if not byte_chunk:
107
- break
108
-
109
- # Mirror raw bytes to actual terminal seamlessly
110
- sys.stdout.buffer.write(byte_chunk)
111
- sys.stdout.buffer.flush()
112
-
113
- char_str = decoder.decode(byte_chunk)
114
- if char_str:
115
- with self.lock:
116
- self.buffer.append(char_str)
117
- except Exception:
118
- pass
119
-
120
- def _eval_loop(self):
121
- while self.running and self.process.poll() is None:
122
- time.sleep(15)
123
-
124
- with self.lock:
125
- if not self.buffer:
126
- continue
127
- full_text = "".join(self.buffer)
128
-
129
- # Flaw 1 Fix: Sliding window overlap to prevent bridging blindness
130
- overlap = full_text[-100:] if len(full_text) > 100 else ""
131
- self.buffer.clear()
132
- if overlap:
133
- self.buffer.append(overlap)
134
-
135
- clean_text = strip_ansi(full_text)
136
- self.observer.log_action(
137
- "CLI_Agent", clean_text, "cli_execution", len(clean_text) // 4
138
- )
139
-
140
- try:
141
- self.observer._evaluate_state("CLI_Agent")
142
- except InterventionException as e:
143
- print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
144
- print(f"[REASON]: {e.args[0]}")
145
- print(
146
- f"[LIVE INJECTION]: Interrupting agent and injecting correction...\n"
147
- )
148
-
149
- # Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
150
- try:
151
- if sys.platform != "win32":
152
- self.process.send_signal(signal.SIGINT)
153
- # Give the agent a split second to catch the interrupt before writing to stdin
154
- time.sleep(0.5)
155
- except Exception:
156
- pass
157
-
158
- try:
159
- # Write the injection payload as binary
160
- payload = (e.course_correction + "\n").encode("utf-8")
161
- self.process.stdin.write(payload)
162
- self.process.stdin.flush()
163
- except Exception as ex:
164
- print(
165
- f"[ERROR] Live injection failed or agent deadlocked: {ex}. Hard terminating."
166
- )
167
- self.process.terminate()
168
- self.running = False
169
- break
170
-
171
-
172
- def main():
173
- try:
174
- from dotenv import load_dotenv
175
-
176
- load_dotenv()
177
- except ImportError:
178
- pass
179
-
180
- parser = argparse.ArgumentParser(
181
- description="Panopticon: Production-Grade Immune System for AI CLIs"
182
- )
183
- parser.add_argument(
184
- "command", nargs=argparse.REMAINDER, help="The command to run, e.g., 'claude'"
185
- )
186
- args = parser.parse_args()
187
-
188
- if not args.command:
189
- print("Usage: panopticon [command]")
190
- sys.exit(1)
191
-
192
- wrapper = CLIWrapper(args.command)
193
- wrapper.run()
194
-
195
-
196
- if __name__ == "__main__":
197
- main()
@@ -1,15 +0,0 @@
1
- from panopticon.cli import strip_ansi
2
-
3
-
4
- def test_strip_ansi():
5
- # Basic red color text
6
- colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
7
- assert strip_ansi(colored_text) == "Fatal Error: File not found"
8
-
9
- # Complex formatting (Bold + Green)
10
- complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
11
- assert strip_ansi(complex_text) == "Success"
12
-
13
- # Loading spinner edge case
14
- spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
15
- assert "Loading..." in strip_ansi(spinner_text)
File without changes
File without changes