panopticon-cli 1.1.3__tar.gz → 1.1.6__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 (23) hide show
  1. {panopticon_cli-1.1.3/panopticon_cli.egg-info → panopticon_cli-1.1.6}/PKG-INFO +1 -1
  2. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/cli.py +196 -37
  3. panopticon_cli-1.1.6/panopticon/conpty.py +211 -0
  4. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6/panopticon_cli.egg-info}/PKG-INFO +1 -1
  5. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/SOURCES.txt +1 -0
  6. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/pyproject.toml +1 -1
  7. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_cli.py +7 -6
  8. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/LICENSE +0 -0
  9. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/README.md +0 -0
  10. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/__init__.py +0 -0
  11. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/memory.py +0 -0
  12. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/observer.py +0 -0
  13. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/policies.py +0 -0
  14. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon/sentinel.py +0 -0
  15. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/dependency_links.txt +0 -0
  16. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/entry_points.txt +0 -0
  17. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/requires.txt +0 -0
  18. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/panopticon_cli.egg-info/top_level.txt +0 -0
  19. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/setup.cfg +0 -0
  20. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_memory.py +0 -0
  21. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_observer.py +0 -0
  22. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/tests/test_policies.py +0 -0
  23. {panopticon_cli-1.1.3 → panopticon_cli-1.1.6}/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.3
3
+ Version: 1.1.6
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -32,6 +32,9 @@ class CLIWrapper:
32
32
  )
33
33
 
34
34
  self.process = None
35
+ self.master_fd = None
36
+ self.master_fd_in = None
37
+ self.conpty_hPC = None
35
38
  self.buffer = []
36
39
  self.running = False
37
40
  self.lock = threading.Lock()
@@ -51,27 +54,81 @@ class CLIWrapper:
51
54
  f"AdversarialLogic"
52
55
  )
53
56
 
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,
57
+ use_pty = (
58
+ sys.platform != "win32"
59
+ and sys.stdin is not None
60
+ and sys.stdin.isatty()
61
+ and sys.stdout.isatty()
62
62
  )
63
+ use_conpty = (
64
+ sys.platform == "win32"
65
+ and sys.stdin is not None
66
+ and sys.stdin.isatty()
67
+ and sys.stdout.isatty()
68
+ )
69
+ self.master_fd = None
70
+ self.master_fd_in = None
71
+ self.conpty_hPC = None
72
+
73
+ if use_pty:
74
+ import pty
75
+
76
+ master_fd, slave_fd = pty.openpty()
77
+ self.master_fd = master_fd
78
+ self.master_fd_in = master_fd
79
+ self.process = subprocess.Popen(
80
+ self.command,
81
+ stdout=slave_fd,
82
+ stderr=slave_fd,
83
+ stdin=slave_fd,
84
+ bufsize=0,
85
+ env=env,
86
+ close_fds=True,
87
+ )
88
+ os.close(slave_fd)
89
+ use_stdin_thread = True
90
+ elif use_conpty:
91
+ from panopticon.conpty import spawn_conpty
92
+
93
+ proc, hPC, master_in, master_out = spawn_conpty(self.command, env=env)
94
+ self.process = proc
95
+ self.conpty_hPC = hPC
96
+ self.master_fd = master_out
97
+ self.master_fd_in = master_in
98
+ use_stdin_thread = True
99
+ else:
100
+ stdin_target = subprocess.PIPE
101
+ use_stdin_thread = True
102
+
103
+ if (
104
+ sys.platform != "win32"
105
+ and sys.stdin is not None
106
+ and sys.stdin.isatty()
107
+ ):
108
+ stdin_target = sys.stdin
109
+ use_stdin_thread = False
110
+
111
+ self.process = subprocess.Popen(
112
+ self.command,
113
+ stdout=subprocess.PIPE,
114
+ stderr=subprocess.STDOUT,
115
+ stdin=stdin_target,
116
+ bufsize=0,
117
+ env=env,
118
+ )
63
119
 
64
- t_out = threading.Thread(target=self._read_stdout)
120
+ t_out = threading.Thread(target=self._read_stdout, daemon=True)
65
121
  t_out.start()
66
122
  self.threads.append(t_out)
67
123
 
68
- t_eval = threading.Thread(target=self._eval_loop)
124
+ t_eval = threading.Thread(target=self._eval_loop, daemon=True)
69
125
  t_eval.start()
70
126
  self.threads.append(t_eval)
71
127
 
72
- t_in = threading.Thread(target=self._read_stdin)
73
- t_in.start()
74
- self.threads.append(t_in)
128
+ if use_stdin_thread:
129
+ t_in = threading.Thread(target=self._read_stdin, daemon=True)
130
+ t_in.start()
131
+ self.threads.append(t_in)
75
132
 
76
133
  # Register cleanup on exit
77
134
  atexit.register(self._cleanup)
@@ -89,6 +146,29 @@ class CLIWrapper:
89
146
  self.running = False
90
147
  self._stop_event.set()
91
148
 
149
+ if getattr(self, "conpty_hPC", None) is not None:
150
+ try:
151
+ from panopticon.conpty import close_conpty
152
+ close_conpty(self.conpty_hPC)
153
+ except Exception:
154
+ pass
155
+ self.conpty_hPC = None
156
+
157
+ if getattr(self, "master_fd_in", None) is not None:
158
+ try:
159
+ if self.master_fd_in != getattr(self, "master_fd", None):
160
+ os.close(self.master_fd_in)
161
+ except Exception:
162
+ pass
163
+ self.master_fd_in = None
164
+
165
+ if getattr(self, "master_fd", None) is not None:
166
+ try:
167
+ os.close(self.master_fd)
168
+ except Exception:
169
+ pass
170
+ self.master_fd = None
171
+
92
172
  # Close subprocess pipes FIRST before thread cleanup
93
173
  if self.process:
94
174
  try:
@@ -118,31 +198,16 @@ class CLIWrapper:
118
198
  if thread.is_alive():
119
199
  thread.join(timeout=1)
120
200
 
121
- def _stdin_ready(self) -> bool:
122
- stdin_obj = sys.stdin
123
- if sys.platform == "win32":
124
- try:
125
- import msvcrt
126
- except ImportError:
127
- pass
128
- else:
129
- if stdin_obj.isatty():
130
- return msvcrt.kbhit()
131
-
132
- try:
133
- import select
134
-
135
- return bool(select.select([stdin_obj], [], [], 0.1)[0])
136
- except Exception:
137
- return False
138
-
139
201
  def _read_stdout(self):
140
202
  # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
141
203
  decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
142
204
  try:
143
205
  while self.running and self.process and self.process.poll() is None:
144
206
  try:
145
- byte_chunk = self.process.stdout.read(1)
207
+ if getattr(self, "master_fd", None) is not None:
208
+ byte_chunk = os.read(self.master_fd, 1)
209
+ else:
210
+ byte_chunk = self.process.stdout.read(1)
146
211
  if not byte_chunk:
147
212
  break
148
213
 
@@ -161,7 +226,8 @@ class CLIWrapper:
161
226
  finally:
162
227
  try:
163
228
  if (
164
- self.process
229
+ getattr(self, "master_fd", None) is None
230
+ and self.process
165
231
  and self.process.stdout
166
232
  and (not self.process.stdout.closed)
167
233
  ):
@@ -169,7 +235,40 @@ class CLIWrapper:
169
235
  except Exception:
170
236
  pass
171
237
 
238
+ def _stdin_ready(self) -> bool:
239
+ if sys.platform == "win32":
240
+ if (
241
+ sys.stdin is not None
242
+ and getattr(sys.stdin, "isatty", lambda: False)()
243
+ and hasattr(sys.stdin, "fileno")
244
+ ):
245
+ try:
246
+ import msvcrt
247
+
248
+ return msvcrt.kbhit()
249
+ except ImportError:
250
+ return False
251
+ return True
252
+ else:
253
+ import select
254
+
255
+ try:
256
+ rlist, _, _ = select.select([sys.stdin], [], [], 0)
257
+ return bool(rlist)
258
+ except Exception:
259
+ return False
260
+
172
261
  def _read_stdin(self):
262
+ if sys.platform == "win32":
263
+ if (
264
+ sys.stdin is not None
265
+ and getattr(sys.stdin, "isatty", lambda: False)()
266
+ and hasattr(sys.stdin, "fileno")
267
+ and sys.stdin.fileno() == 0
268
+ ):
269
+ self._read_stdin_windows()
270
+ return
271
+
173
272
  stdin_buffer = getattr(sys.stdin, "buffer", None)
174
273
  if stdin_buffer is None:
175
274
  return
@@ -186,10 +285,66 @@ class CLIWrapper:
186
285
  continue
187
286
 
188
287
  try:
189
- data = stdin_buffer.read1(1024)
288
+ read_func = getattr(stdin_buffer, "read1", stdin_buffer.read)
289
+ data = read_func(1024)
190
290
  if not data:
191
291
  break
192
- if self.process.stdin and not self.process.stdin.closed:
292
+ if getattr(self, "master_fd_in", None) is not None:
293
+ os.write(self.master_fd_in, data)
294
+ elif getattr(self, "master_fd", None) is not None:
295
+ os.write(self.master_fd, data)
296
+ elif self.process.stdin and not self.process.stdin.closed:
297
+ self.process.stdin.write(data)
298
+ self.process.stdin.flush()
299
+ except (OSError, ValueError, BrokenPipeError):
300
+ break
301
+ except Exception:
302
+ pass
303
+ finally:
304
+ try:
305
+ if (
306
+ getattr(self, "master_fd", None) is None
307
+ and self.process
308
+ and self.process.stdin
309
+ and (not self.process.stdin.closed)
310
+ ):
311
+ self.process.stdin.close()
312
+ except Exception:
313
+ pass
314
+
315
+ def _read_stdin_windows(self):
316
+ try:
317
+ import msvcrt
318
+ except ImportError:
319
+ return
320
+
321
+ try:
322
+ while (
323
+ self.running
324
+ and self.process
325
+ and self.process.poll() is None
326
+ and not self._stop_event.is_set()
327
+ ):
328
+ if not msvcrt.kbhit():
329
+ time.sleep(0.1)
330
+ continue
331
+
332
+ try:
333
+ char = msvcrt.getwch()
334
+ if char == "\x03":
335
+ if self.process.poll() is None:
336
+ try:
337
+ self.process.send_signal(signal.CTRL_C_EVENT)
338
+ except Exception:
339
+ pass
340
+ continue
341
+
342
+ data = char.encode("utf-8")
343
+ if getattr(self, "master_fd_in", None) is not None:
344
+ os.write(self.master_fd_in, data)
345
+ elif getattr(self, "master_fd", None) is not None:
346
+ os.write(self.master_fd, data)
347
+ elif self.process.stdin and not self.process.stdin.closed:
193
348
  self.process.stdin.write(data)
194
349
  self.process.stdin.flush()
195
350
  except (OSError, ValueError, BrokenPipeError):
@@ -248,8 +403,12 @@ class CLIWrapper:
248
403
 
249
404
  try:
250
405
  # Write the injection payload as binary
251
- if self.process.stdin and not self.process.stdin.closed:
252
- payload = (e.course_correction + "\n").encode("utf-8")
406
+ payload = (e.course_correction + "\n").encode("utf-8")
407
+ if getattr(self, "master_fd_in", None) is not None:
408
+ os.write(self.master_fd_in, payload)
409
+ elif getattr(self, "master_fd", None) is not None:
410
+ os.write(self.master_fd, payload)
411
+ elif self.process.stdin and not self.process.stdin.closed:
253
412
  self.process.stdin.write(payload)
254
413
  self.process.stdin.flush()
255
414
  except Exception as ex:
@@ -0,0 +1,211 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import shutil
5
+ import time
6
+
7
+ if sys.platform == "win32":
8
+ import ctypes
9
+ from ctypes import wintypes
10
+ import msvcrt
11
+
12
+ kernel32 = ctypes.windll.kernel32
13
+
14
+ class COORD(ctypes.Structure):
15
+ _fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
16
+
17
+ class STARTUPINFOW(ctypes.Structure):
18
+ _fields_ = [
19
+ ("cb", wintypes.DWORD),
20
+ ("lpReserved", wintypes.LPWSTR),
21
+ ("lpDesktop", wintypes.LPWSTR),
22
+ ("lpTitle", wintypes.LPWSTR),
23
+ ("dwX", wintypes.DWORD),
24
+ ("dwY", wintypes.DWORD),
25
+ ("dwXSize", wintypes.DWORD),
26
+ ("dwYSize", wintypes.DWORD),
27
+ ("dwXCountChars", wintypes.DWORD),
28
+ ("dwYCountChars", wintypes.DWORD),
29
+ ("dwFillAttribute", wintypes.DWORD),
30
+ ("dwFlags", wintypes.DWORD),
31
+ ("wShowWindow", wintypes.WORD),
32
+ ("cbReserved2", wintypes.WORD),
33
+ ("lpReserved2", ctypes.POINTER(ctypes.c_byte)),
34
+ ("hStdInput", wintypes.HANDLE),
35
+ ("hStdOutput", wintypes.HANDLE),
36
+ ("hStdError", wintypes.HANDLE),
37
+ ]
38
+
39
+ class STARTUPINFOEXW(ctypes.Structure):
40
+ _fields_ = [
41
+ ("StartupInfo", STARTUPINFOW),
42
+ ("lpAttributeList", wintypes.LPVOID),
43
+ ]
44
+
45
+ class PROCESS_INFORMATION(ctypes.Structure):
46
+ _fields_ = [
47
+ ("hProcess", wintypes.HANDLE),
48
+ ("hThread", wintypes.HANDLE),
49
+ ("dwProcessId", wintypes.DWORD),
50
+ ("dwThreadId", wintypes.DWORD),
51
+ ]
52
+
53
+ HPCON = wintypes.HANDLE
54
+ EXTENDED_STARTUPINFO_PRESENT = 0x00080000
55
+ CREATE_UNICODE_ENVIRONMENT = 0x00000400
56
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
57
+
58
+ class ConPTYProcess:
59
+ def __init__(self, pi, hPC):
60
+ self.pi = pi
61
+ self.hPC = hPC
62
+ self.returncode = None
63
+ self.stdin = None
64
+ self.stdout = None
65
+ self.stderr = None
66
+
67
+ def poll(self):
68
+ if self.returncode is not None:
69
+ return self.returncode
70
+ res = kernel32.WaitForSingleObject(self.pi.hProcess, 0)
71
+ if res == 0: # WAIT_OBJECT_0
72
+ exit_code = wintypes.DWORD()
73
+ if kernel32.GetExitCodeProcess(self.pi.hProcess, ctypes.byref(exit_code)):
74
+ self.returncode = exit_code.value
75
+ else:
76
+ self.returncode = 0
77
+ self.cleanup_handles()
78
+ return self.returncode
79
+
80
+ def wait(self, timeout=None):
81
+ if self.returncode is not None:
82
+ return self.returncode
83
+ if timeout is None:
84
+ ms = 0xFFFFFFFF # INFINITE
85
+ else:
86
+ ms = int(timeout * 1000)
87
+ res = kernel32.WaitForSingleObject(self.pi.hProcess, ms)
88
+ if res == 0:
89
+ exit_code = wintypes.DWORD()
90
+ if kernel32.GetExitCodeProcess(self.pi.hProcess, ctypes.byref(exit_code)):
91
+ self.returncode = exit_code.value
92
+ else:
93
+ self.returncode = 0
94
+ self.cleanup_handles()
95
+ return self.returncode
96
+ elif res == 0x00000102: # WAIT_TIMEOUT
97
+ raise subprocess.TimeoutExpired(self.pi.dwProcessId, timeout)
98
+ return self.returncode
99
+
100
+ def terminate(self):
101
+ if self.poll() is None:
102
+ kernel32.TerminateProcess(self.pi.hProcess, 1)
103
+
104
+ def kill(self):
105
+ self.terminate()
106
+
107
+ def send_signal(self, sig):
108
+ pass
109
+
110
+ def cleanup_handles(self):
111
+ if getattr(self, "pi", None) and self.pi.hProcess:
112
+ kernel32.CloseHandle(self.pi.hProcess)
113
+ self.pi.hProcess = 0
114
+ if getattr(self, "pi", None) and self.pi.hThread:
115
+ kernel32.CloseHandle(self.pi.hThread)
116
+ self.pi.hThread = 0
117
+
118
+ def spawn_conpty(command, env=None):
119
+ in_read, in_write = os.pipe()
120
+ out_read, out_write = os.pipe()
121
+
122
+ hInputRead = msvcrt.get_osfhandle(in_read)
123
+ hOutputWrite = msvcrt.get_osfhandle(out_write)
124
+
125
+ size = COORD(120, 30)
126
+ hPC = HPCON()
127
+ res = kernel32.CreatePseudoConsole(size, hInputRead, hOutputWrite, 0, ctypes.byref(hPC))
128
+ if res != 0:
129
+ os.close(in_read)
130
+ os.close(in_write)
131
+ os.close(out_read)
132
+ os.close(out_write)
133
+ raise RuntimeError(f"CreatePseudoConsole failed with HRESULT {res}")
134
+
135
+ attr_list_size = ctypes.c_size_t(0)
136
+ kernel32.InitializeProcThreadAttributeList(None, 1, 0, ctypes.byref(attr_list_size))
137
+ attr_list = ctypes.create_string_buffer(attr_list_size.value)
138
+ if not kernel32.InitializeProcThreadAttributeList(attr_list, 1, 0, ctypes.byref(attr_list_size)):
139
+ kernel32.ClosePseudoConsole(hPC)
140
+ raise RuntimeError("InitializeProcThreadAttributeList failed")
141
+
142
+ if not kernel32.UpdateProcThreadAttribute(
143
+ attr_list,
144
+ 0,
145
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
146
+ hPC,
147
+ ctypes.sizeof(HPCON),
148
+ None,
149
+ None,
150
+ ):
151
+ kernel32.DeleteProcThreadAttributeList(attr_list)
152
+ kernel32.ClosePseudoConsole(hPC)
153
+ raise RuntimeError("UpdateProcThreadAttribute failed")
154
+
155
+ si_ex = STARTUPINFOEXW()
156
+ si_ex.StartupInfo.cb = ctypes.sizeof(STARTUPINFOEXW)
157
+ si_ex.lpAttributeList = ctypes.cast(attr_list, wintypes.LPVOID)
158
+
159
+ pi = PROCESS_INFORMATION()
160
+
161
+ exe = shutil.which(command[0]) or command[0]
162
+ cmdline = subprocess.list2cmdline([exe] + command[1:])
163
+ cmd_buf = ctypes.create_unicode_buffer(cmdline)
164
+
165
+ env_buf = None
166
+ flags = EXTENDED_STARTUPINFO_PRESENT
167
+ if env is not None:
168
+ env_items = [f"{str(k)}={str(v)}" for k, v in env.items()]
169
+ env_str = "\0".join(sorted(env_items)) + "\0\0"
170
+ env_buf = ctypes.create_unicode_buffer(env_str)
171
+ flags |= CREATE_UNICODE_ENVIRONMENT
172
+
173
+ success = kernel32.CreateProcessW(
174
+ None,
175
+ cmd_buf,
176
+ None,
177
+ None,
178
+ False,
179
+ flags,
180
+ env_buf,
181
+ None,
182
+ ctypes.byref(si_ex),
183
+ ctypes.byref(pi),
184
+ )
185
+
186
+ kernel32.DeleteProcThreadAttributeList(attr_list)
187
+ os.close(in_read)
188
+ os.close(out_write)
189
+
190
+ if not success:
191
+ err = kernel32.GetLastError()
192
+ kernel32.ClosePseudoConsole(hPC)
193
+ os.close(in_write)
194
+ os.close(out_read)
195
+ raise RuntimeError(f"CreateProcessW failed with error {err}")
196
+
197
+ proc = ConPTYProcess(pi, hPC)
198
+ return proc, hPC, in_write, out_read
199
+
200
+ def close_conpty(hPC):
201
+ if hPC:
202
+ try:
203
+ kernel32.ClosePseudoConsole(hPC)
204
+ except Exception:
205
+ pass
206
+ else:
207
+ def spawn_conpty(command, env=None):
208
+ raise NotImplementedError("ConPTY is only supported on Windows")
209
+
210
+ def close_conpty(hPC):
211
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.3
3
+ Version: 1.1.6
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  panopticon/__init__.py
5
5
  panopticon/cli.py
6
+ panopticon/conpty.py
6
7
  panopticon/memory.py
7
8
  panopticon/observer.py
8
9
  panopticon/policies.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "panopticon-cli"
7
- version = "1.1.3"
7
+ version = "1.1.6"
8
8
  description = "The Cognitive Immune System for Autonomous CLI Agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import sys
3
+ from unittest.mock import patch
3
4
 
4
5
  from panopticon.cli import CLIWrapper, strip_ansi
5
6
 
@@ -52,12 +53,12 @@ def test_read_stdin_forwards_input():
52
53
  wrapper.running = True
53
54
  wrapper.process = DummyProcess()
54
55
 
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
56
+ dummy_stdin = DummyStdinStream(b"hello world\n")
57
+ dummy_stdin.isatty = lambda: True
58
+
59
+ with patch("panopticon.cli.sys.stdin", dummy_stdin):
60
+ with patch.object(CLIWrapper, "_stdin_ready", return_value=True):
61
+ wrapper._read_stdin()
61
62
 
62
63
  assert wrapper.process.stdin.written == b"hello world\n"
63
64
  assert wrapper.process.stdin.closed is True
File without changes
File without changes
File without changes