dcc-mcp-renderdoc 0.3.1__tar.gz → 0.3.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 (34) hide show
  1. dcc_mcp_renderdoc-0.3.2/.release-please-manifest.json +2 -0
  2. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/CHANGELOG.md +7 -0
  3. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/PKG-INFO +1 -1
  4. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/pyproject.toml +1 -1
  5. dcc_mcp_renderdoc-0.3.2/src/dcc_mcp_renderdoc/__version__.py +3 -0
  6. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/runtime.py +114 -28
  7. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +3 -1
  8. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +1 -1
  9. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_runtime.py +177 -15
  10. dcc_mcp_renderdoc-0.3.1/.release-please-manifest.json +0 -2
  11. dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/__version__.py +0 -3
  12. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/.gitignore +0 -0
  13. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/LICENSE +0 -0
  14. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/README.md +0 -0
  15. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/release-please-config.json +0 -0
  16. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/__init__.py +0 -0
  17. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/server.py +0 -0
  18. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +0 -0
  19. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
  20. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
  21. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
  22. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
  23. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +0 -0
  24. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
  25. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
  26. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
  27. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
  28. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/live_renderdoc_smoke.py +0 -0
  29. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/renderdoc_smoke.c +0 -0
  30. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_download.py +0 -0
  31. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tests/test_package.py +0 -0
  32. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/__init__.py +0 -0
  33. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/download_latest_renderdoc.py +0 -0
  34. {dcc_mcp_renderdoc-0.3.1 → dcc_mcp_renderdoc-0.3.2}/tools/lint_skills.py +0 -0
@@ -0,0 +1,2 @@
1
+ {".":"0.3.2"}
2
+
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.1...v0.3.2) (2026-07-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * keep capture controller alive through trigger ([#11](https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues/11)) ([1fb7bed](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/1fb7bed40dfb027b148a14cefd01c53028891d56))
9
+
3
10
  ## [0.3.1](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.0...v0.3.1) (2026-07-22)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcc-mcp-renderdoc
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: RenderDoc capture and replay automation skills for DCC-MCP
5
5
  Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
6
6
  Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dcc-mcp-renderdoc"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "RenderDoc capture and replay automation skills for DCC-MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,3 @@
1
+ """Package version."""
2
+
3
+ __version__ = "0.3.2" # x-release-please-version
@@ -8,6 +8,7 @@ import shutil
8
8
  import subprocess
9
9
  import sys
10
10
  import tempfile
11
+ import threading
11
12
  import time
12
13
  import xml.etree.ElementTree as ET
13
14
  from collections import Counter
@@ -19,6 +20,81 @@ class RenderDocError(RuntimeError):
19
20
  """Raised when a RenderDoc operation cannot satisfy its contract."""
20
21
 
21
22
 
23
+ class _CaptureController:
24
+ def __init__(self, process: Any, stdout: list[str], stderr: list[str], readers: list[Any]):
25
+ self.process = process
26
+ self.stdout = stdout
27
+ self.stderr = stderr
28
+ self.readers = readers
29
+
30
+ def output(self) -> tuple[str, str]:
31
+ return "".join(self.stdout), "".join(self.stderr)
32
+
33
+ def close(self) -> None:
34
+ if self.process.poll() is None:
35
+ self.process.terminate()
36
+ try:
37
+ self.process.wait(timeout=5)
38
+ except subprocess.TimeoutExpired:
39
+ self.process.kill()
40
+ self.process.wait(timeout=5)
41
+ for reader in self.readers:
42
+ reader.join(timeout=1)
43
+
44
+
45
+ def _start_capture_controller(
46
+ arguments: Sequence[str], *, timeout_secs: int, command: Optional[str]
47
+ ) -> _CaptureController:
48
+ executable = resolve_renderdoccmd(command)
49
+ try:
50
+ process = subprocess.Popen(
51
+ [str(executable), *arguments],
52
+ stdout=subprocess.PIPE,
53
+ stderr=subprocess.PIPE,
54
+ text=True,
55
+ shell=False,
56
+ )
57
+ except OSError as exc:
58
+ raise RenderDocError(f"Could not start RenderDoc: {exc}") from exc
59
+
60
+ stdout: list[str] = []
61
+ stderr: list[str] = []
62
+
63
+ def read_stream(stream: Any, output: list[str]) -> None:
64
+ for chunk in iter(lambda: stream.read(1), ""):
65
+ output.append(chunk)
66
+
67
+ readers = [
68
+ threading.Thread(target=read_stream, args=(process.stdout, stdout), daemon=True),
69
+ threading.Thread(target=read_stream, args=(process.stderr, stderr), daemon=True),
70
+ ]
71
+ for reader in readers:
72
+ reader.start()
73
+ controller = _CaptureController(process, stdout, stderr, readers)
74
+ deadline = time.monotonic() + timeout_secs
75
+ while True:
76
+ combined = "".join(stdout + stderr)
77
+ launched = re.search(r"Launched as ID (\d+)", combined)
78
+ returncode = process.poll()
79
+ if launched is not None:
80
+ launched_id = int(launched.group(1))
81
+ if returncode is None or returncode in (0, launched_id):
82
+ return controller
83
+ if returncode is not None:
84
+ controller.close()
85
+ detail = "".join(controller.output()).strip()[-2000:] or "unknown error"
86
+ raise RenderDocError(f"RenderDoc exited with code {returncode}: {detail}")
87
+ if time.monotonic() >= deadline:
88
+ controller.close()
89
+ detail = "".join(controller.output()).strip()[-2000:]
90
+ suffix = f": {detail}" if detail else ""
91
+ raise RenderDocError(
92
+ f"RenderDoc command timed out waiting for {arguments[0]} readiness after "
93
+ f"{timeout_secs}s{suffix}"
94
+ )
95
+ time.sleep(0.01)
96
+
97
+
22
98
  def resolve_renderdoccmd(explicit: Optional[str] = None) -> Path:
23
99
  """Resolve an executable RenderDoc CLI without invoking a shell."""
24
100
  candidate = explicit or os.environ.get("DCC_MCP_RENDERDOC_CMD")
@@ -115,23 +191,33 @@ def capture_program(
115
191
  if hook_children:
116
192
  cli_args.append("--opt-hook-children")
117
193
  cli_args.extend([str(target), *(str(value) for value in arguments or [])])
118
- result = _run(
119
- cli_args,
120
- timeout_secs=timeout_secs,
121
- command=command,
122
- accept_launched_id=not effective_wait_for_exit,
123
- )
124
194
  target_pid: Optional[int] = None
125
195
  if trigger_after_secs is not None:
126
- time.sleep(max(0.0, trigger_after_secs))
127
- target_pid, focused, captures = _wait_for_triggered_capture(
128
- output.parent,
129
- before,
130
- process_name,
131
- visible_before_launch,
132
- capture_wait_secs,
196
+ controller = _start_capture_controller(
197
+ cli_args,
198
+ timeout_secs=timeout_secs,
199
+ command=command,
133
200
  )
201
+ try:
202
+ time.sleep(max(0.0, trigger_after_secs))
203
+ target_pid, focused, captures = _wait_for_triggered_capture(
204
+ output.parent,
205
+ before,
206
+ process_name,
207
+ visible_before_launch,
208
+ capture_wait_secs,
209
+ )
210
+ finally:
211
+ controller.close()
212
+ stdout, stderr = controller.output()
134
213
  else:
214
+ result = _run(
215
+ cli_args,
216
+ timeout_secs=timeout_secs,
217
+ command=command,
218
+ accept_launched_id=not effective_wait_for_exit,
219
+ )
220
+ stdout, stderr = result.stdout, result.stderr
135
221
  focused = False
136
222
  captures = _new_captures(output.parent, before)
137
223
  injection_error: Optional[str] = None
@@ -157,9 +243,7 @@ def capture_program(
157
243
  except RenderDocError as exc:
158
244
  injection_error = str(exc)
159
245
  if not captures:
160
- output_detail = "\n".join(
161
- part.strip() for part in (result.stdout, result.stderr) if part.strip()
162
- )[-2000:]
246
+ output_detail = "\n".join(part.strip() for part in (stdout, stderr) if part.strip())[-2000:]
163
247
  diagnostics = _capture_failure_diagnostics(
164
248
  process_id=target_pid,
165
249
  process_name=trigger_process_name or target.name,
@@ -175,7 +259,7 @@ def capture_program(
175
259
  "target": str(target),
176
260
  "captures": [str(path) for path in captures],
177
261
  "focused_target_window": focused,
178
- "stdout": result.stdout.strip(),
262
+ "stdout": stdout.strip(),
179
263
  }
180
264
 
181
265
 
@@ -204,34 +288,36 @@ def capture_process(
204
288
  cli_args.extend(["--working-dir", str(cwd)])
205
289
  if api_validation:
206
290
  cli_args.append("--opt-api-validation")
207
- result = _run(
291
+ controller = _start_capture_controller(
208
292
  cli_args,
209
293
  timeout_secs=timeout_secs,
210
294
  command=command,
211
- accept_launched_id=True,
212
295
  )
213
- time.sleep(max(0.0, trigger_after_secs))
214
- focused = _trigger_capture_hotkey(process_id)
215
- captures = _wait_for_captures(output.parent, before, capture_wait_secs)
296
+ try:
297
+ time.sleep(max(0.0, trigger_after_secs))
298
+ focused = _trigger_capture_hotkey(process_id)
299
+ captures = _wait_for_captures(output.parent, before, capture_wait_secs)
300
+ finally:
301
+ controller.close()
302
+ stdout, stderr = controller.output()
216
303
  if not captures:
217
- output_detail = "\n".join(
218
- part.strip() for part in (result.stdout, result.stderr) if part.strip()
219
- )[-2000:]
304
+ output_detail = "\n".join(part.strip() for part in (stdout, stderr) if part.strip())[-2000:]
220
305
  diagnostics = _capture_failure_diagnostics(
221
306
  process_id=process_id,
222
307
  process_name=None,
223
308
  focused=focused,
224
309
  )
225
310
  raise RenderDocError(
226
- "No .rdc capture appeared after injection and F12; ensure the target window is visible "
227
- f"and uses a graphics API supported by RenderDoc. {diagnostics}. "
311
+ "No .rdc capture appeared after injection and F12. Inject before the target creates "
312
+ "its graphics device; for an already initialized game, relaunch it with "
313
+ f"capture_program. {diagnostics}. "
228
314
  f"RenderDoc output: {output_detail}"
229
315
  )
230
316
  return {
231
317
  "process_id": process_id,
232
318
  "captures": [str(path) for path in captures],
233
319
  "focused_target_window": focused,
234
- "stdout": result.stdout.strip(),
320
+ "stdout": stdout.strip(),
235
321
  }
236
322
 
237
323
 
@@ -27,4 +27,6 @@ focus before F12.
27
27
 
28
28
  Use `capture_process` when Steam or another platform client must launch the target first. It
29
29
  injects into the explicit PID, focuses that visible window, triggers F12 after the requested delay,
30
- and waits for the resulting capture. Neither tool terminates the target process.
30
+ and waits for the resulting capture. Inject before the target creates its graphics device; late
31
+ injection cannot recover resources that RenderDoc did not observe. Prefer `capture_program` when
32
+ the executable can be launched directly. Neither tool terminates the target process.
@@ -46,7 +46,7 @@ tools:
46
46
  on-success: [renderdoc_analysis__inspect_capture, renderdoc_analysis__export_thumbnail]
47
47
  on-failure: [dcc_diagnostics__audit_log]
48
48
  - name: capture_process
49
- description: Inject RenderDoc into one visible Windows process, focus its window, trigger F12 after a delay, and return the new .rdc file. Use when a game must be launched by its platform client. Not for hidden services or process termination.
49
+ description: Inject RenderDoc into one Windows process before it creates its graphics device, focus its visible window, trigger F12 after a delay, and return the new .rdc file. Use when a game must be launched by its platform client. Prefer capture_program for an already initialized game; not for hidden services or process termination.
50
50
  input_schema:
51
51
  type: object
52
52
  required: [process_id, output_template]
@@ -1,11 +1,24 @@
1
+ import io
1
2
  from pathlib import Path
2
- from subprocess import CompletedProcess
3
+ from subprocess import CompletedProcess, TimeoutExpired
3
4
 
4
5
  import pytest
5
6
 
6
7
  from dcc_mcp_renderdoc import runtime
7
8
 
8
9
 
10
+ class StubController:
11
+ def __init__(self, stdout="Launched as ID 123", stderr=""):
12
+ self.stdout = stdout
13
+ self.stderr = stderr
14
+
15
+ def output(self):
16
+ return self.stdout, self.stderr
17
+
18
+ def close(self):
19
+ return None
20
+
21
+
9
22
  def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
10
23
  xml_file = tmp_path / "capture.xml"
11
24
  xml_file.write_text(
@@ -76,10 +89,9 @@ def test_capture_process_injects_triggers_and_reports_capture(tmp_path, monkeypa
76
89
 
77
90
  def fake_run(arguments, **kwargs):
78
91
  observed["arguments"] = arguments
79
- observed["accept_launched_id"] = kwargs["accept_launched_id"]
80
- return CompletedProcess(arguments, 456, "Launched as ID 456", "")
92
+ return StubController("Launched as ID 456")
81
93
 
82
- monkeypatch.setattr(runtime, "_run", fake_run)
94
+ monkeypatch.setattr(runtime, "_start_capture_controller", fake_run)
83
95
  monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda pid: pid == 42)
84
96
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
85
97
  monkeypatch.setattr(
@@ -91,18 +103,168 @@ def test_capture_process_injects_triggers_and_reports_capture(tmp_path, monkeypa
91
103
  result = runtime.capture_process(42, str(tmp_path / "capture"))
92
104
 
93
105
  assert observed["arguments"][:2] == ["inject", "--PID=42"]
94
- assert observed["accept_launched_id"] is True
95
106
  assert result["focused_target_window"] is True
96
107
 
97
108
 
109
+ def test_capture_process_triggers_while_injector_is_running_and_only_stops_injector(
110
+ tmp_path, monkeypatch
111
+ ):
112
+ started = {}
113
+ target = {"running": True}
114
+ command = tmp_path / "renderdoccmd.exe"
115
+ command.touch()
116
+
117
+ class Injector:
118
+ def __init__(self, arguments, **kwargs):
119
+ started.update(arguments=arguments, kwargs=kwargs, process=self)
120
+ self.stdout = io.StringIO("Launched as ID 42\n")
121
+ self.stderr = io.StringIO("")
122
+ self.returncode = None
123
+ self.terminated = False
124
+ self.killed = False
125
+
126
+ def poll(self):
127
+ return self.returncode
128
+
129
+ def terminate(self):
130
+ self.terminated = True
131
+ self.returncode = 0
132
+
133
+ def kill(self):
134
+ self.killed = True
135
+ self.returncode = -9
136
+
137
+ def wait(self, timeout=None):
138
+ if self.returncode is None:
139
+ raise TimeoutExpired(self.args, timeout)
140
+ return self.returncode
141
+
142
+ monkeypatch.setattr(runtime.subprocess, "Popen", Injector)
143
+ monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda process_id: process_id == 42)
144
+ monkeypatch.setattr(
145
+ runtime,
146
+ "_wait_for_captures",
147
+ lambda directory, before, timeout: [tmp_path / "capture_frame1.rdc"],
148
+ )
149
+
150
+ result = runtime.capture_process(
151
+ 42,
152
+ str(tmp_path / "capture;still-one-argument"),
153
+ trigger_after_secs=0,
154
+ timeout_secs=1,
155
+ command=str(command),
156
+ )
157
+
158
+ assert result["focused_target_window"] is True
159
+ assert started["arguments"][1:3] == ["inject", "--PID=42"]
160
+ assert started["arguments"][4] == str((tmp_path / "capture;still-one-argument").resolve())
161
+ assert started["kwargs"]["shell"] is False
162
+ assert started["process"].terminated is True
163
+ assert started["process"].killed is False
164
+ assert target["running"] is True
165
+
166
+
167
+ def test_capture_process_readiness_timeout_force_stops_only_injector(tmp_path, monkeypatch):
168
+ command = tmp_path / "renderdoccmd.exe"
169
+ command.touch()
170
+ target = {"running": True}
171
+ started = {}
172
+
173
+ class StuckInjector:
174
+ def __init__(self, arguments, **kwargs):
175
+ started["process"] = self
176
+ self.stdout = io.StringIO("")
177
+ self.stderr = io.StringIO("")
178
+ self.killed = False
179
+ self.terminated = False
180
+
181
+ def poll(self):
182
+ return -9 if self.killed else None
183
+
184
+ def terminate(self):
185
+ self.terminated = True
186
+
187
+ def kill(self):
188
+ self.killed = True
189
+
190
+ def wait(self, timeout=None):
191
+ if not self.killed:
192
+ raise TimeoutExpired("renderdoccmd", timeout)
193
+ return -9
194
+
195
+ monkeypatch.setattr(runtime.subprocess, "Popen", StuckInjector)
196
+
197
+ with pytest.raises(runtime.RenderDocError, match="waiting for inject readiness"):
198
+ runtime.capture_process(
199
+ 42,
200
+ str(tmp_path / "capture"),
201
+ trigger_after_secs=0,
202
+ timeout_secs=0,
203
+ command=str(command),
204
+ )
205
+
206
+ assert started["process"].terminated is True
207
+ assert started["process"].killed is True
208
+ assert target["running"] is True
209
+
210
+
211
+ def test_triggered_program_capture_keeps_controller_alive_until_capture(tmp_path, monkeypatch):
212
+ target = tmp_path / "game.exe"
213
+ target.touch()
214
+ command = tmp_path / "renderdoccmd.exe"
215
+ command.touch()
216
+ events = []
217
+
218
+ class Controller:
219
+ def __init__(self, arguments, **kwargs):
220
+ events.append(("started", arguments, kwargs))
221
+ self.stdout = io.StringIO("Launched as ID 77\n")
222
+ self.stderr = io.StringIO("")
223
+ self.returncode = None
224
+
225
+ def poll(self):
226
+ return self.returncode
227
+
228
+ def terminate(self):
229
+ events.append(("stopped",))
230
+ self.returncode = 0
231
+
232
+ def kill(self):
233
+ raise AssertionError("responsive controller should not be killed")
234
+
235
+ def wait(self, timeout=None):
236
+ return self.returncode
237
+
238
+ monkeypatch.setattr(runtime.subprocess, "Popen", Controller)
239
+ monkeypatch.setattr(runtime, "_visible_process_ids", lambda _name: [])
240
+
241
+ def captured_while_running(directory, before, name, ignored, timeout):
242
+ assert [event[0] for event in events] == ["started"]
243
+ return 77, True, [tmp_path / "capture_frame1.rdc"]
244
+
245
+ monkeypatch.setattr(runtime, "_wait_for_triggered_capture", captured_while_running)
246
+
247
+ result = runtime.capture_program(
248
+ str(target),
249
+ str(tmp_path / "capture"),
250
+ trigger_after_secs=0,
251
+ command=str(command),
252
+ )
253
+
254
+ assert result["captures"] == [str((tmp_path / "capture_frame1.rdc").resolve())]
255
+ assert [event[0] for event in events] == ["started", "stopped"]
256
+ assert events[0][1][-1] == str(target.resolve())
257
+ assert events[0][2]["shell"] is False
258
+
259
+
98
260
  def test_capture_program_focuses_requested_child_before_trigger(tmp_path, monkeypatch):
99
261
  target = tmp_path / "launcher.exe"
100
262
  target.touch()
101
263
  observed = {}
102
264
  monkeypatch.setattr(
103
265
  runtime,
104
- "_run",
105
- lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
266
+ "_start_capture_controller",
267
+ lambda arguments, **kwargs: StubController(),
106
268
  )
107
269
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
108
270
  monkeypatch.setattr(
@@ -131,8 +293,8 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
131
293
  target.touch()
132
294
  monkeypatch.setattr(
133
295
  runtime,
134
- "_run",
135
- lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
296
+ "_start_capture_controller",
297
+ lambda arguments, **kwargs: StubController(),
136
298
  )
137
299
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
138
300
  monkeypatch.setattr(
@@ -203,8 +365,8 @@ def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
203
365
  observed = {}
204
366
  monkeypatch.setattr(
205
367
  runtime,
206
- "_run",
207
- lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
368
+ "_start_capture_controller",
369
+ lambda arguments, **kwargs: StubController(),
208
370
  )
209
371
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
210
372
  monkeypatch.setattr(
@@ -239,10 +401,8 @@ def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
239
401
  def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeypatch):
240
402
  monkeypatch.setattr(
241
403
  runtime,
242
- "_run",
243
- lambda arguments, **kwargs: CompletedProcess(
244
- arguments, 42, "Launched as ID 42", "inject warning"
245
- ),
404
+ "_start_capture_controller",
405
+ lambda arguments, **kwargs: StubController("Launched as ID 42", "inject warning"),
246
406
  )
247
407
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
248
408
  monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda pid: False)
@@ -257,6 +417,8 @@ def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeyp
257
417
  runtime.capture_process(42, str(tmp_path / "capture"))
258
418
 
259
419
  message = str(error.value)
420
+ assert "before the target creates its graphics device" in message
421
+ assert "capture_program" in message
260
422
  assert "target_process=game.exe(pid=42)" in message
261
423
  assert "focused_target_window=False" in message
262
424
  assert "RenderDoc output: Launched as ID 42\ninject warning" in message
@@ -1,2 +0,0 @@
1
- {".":"0.3.1"}
2
-
@@ -1,3 +0,0 @@
1
- """Package version."""
2
-
3
- __version__ = "0.3.1" # x-release-please-version