dcc-mcp-renderdoc 0.3.0__tar.gz → 0.3.1__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.1/.release-please-manifest.json +2 -0
  2. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/CHANGELOG.md +14 -0
  3. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/PKG-INFO +48 -1
  4. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/README.md +47 -0
  5. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/pyproject.toml +1 -1
  6. dcc_mcp_renderdoc-0.3.1/src/dcc_mcp_renderdoc/__version__.py +3 -0
  7. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/runtime.py +63 -20
  8. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +1 -1
  9. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_package.py +16 -0
  10. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_runtime.py +86 -11
  11. dcc_mcp_renderdoc-0.3.0/.release-please-manifest.json +0 -2
  12. dcc_mcp_renderdoc-0.3.0/src/dcc_mcp_renderdoc/__version__.py +0 -3
  13. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/.gitignore +0 -0
  14. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/LICENSE +0 -0
  15. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/release-please-config.json +0 -0
  16. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/__init__.py +0 -0
  17. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/server.py +0 -0
  18. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +0 -0
  19. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +0 -0
  20. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +0 -0
  21. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +0 -0
  22. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +0 -0
  23. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +0 -0
  24. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +0 -0
  25. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +0 -0
  26. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_process.py +0 -0
  27. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +0 -0
  28. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +0 -0
  29. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/live_renderdoc_smoke.py +0 -0
  30. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/renderdoc_smoke.c +0 -0
  31. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tests/test_download.py +0 -0
  32. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/__init__.py +0 -0
  33. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/download_latest_renderdoc.py +0 -0
  34. {dcc_mcp_renderdoc-0.3.0 → dcc_mcp_renderdoc-0.3.1}/tools/lint_skills.py +0 -0
@@ -0,0 +1,2 @@
1
+ {".":"0.3.1"}
2
+
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.3.0...v0.3.1) (2026-07-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * allow long-running frame trigger delays ([#10](https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues/10)) ([3212cf7](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/3212cf7c9f236b28b48bf0d28705e2b1939fbe21))
9
+ * capture relaunched child processes ([39e5603](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/39e5603112c8cad98807da3fee86248dd03fb824))
10
+
11
+
12
+ ### Documentation
13
+
14
+ * align agent workflow and branding ([ca8b2cf](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/ca8b2cf955a470a87efde24c12e9ebbdb284633c))
15
+ * document CLI install and updates ([599cc70](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/599cc70445e11c2fc53d2626c24f9912365b87e0))
16
+
3
17
  ## [0.3.0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.2.1...v0.3.0) (2026-07-16)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dcc-mcp-renderdoc
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -28,6 +28,53 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  # dcc-mcp-renderdoc
30
30
 
31
+ <p align="center">
32
+ <img src="docs/assets/dcc-mcp-renderdoc.svg" alt="DCC-MCP · RENDERDOC" width="600">
33
+ </p>
34
+
35
+ ## Agent workflow
36
+
37
+ AI agents should use the shared gateway through `dcc-mcp-cli`; IDE users may
38
+ continue to use the MCP endpoint. Prefer typed skills and tools over raw scripts.
39
+
40
+ ### Install or update the CLI
41
+
42
+ `dcc-mcp-cli` is the preferred control path for every shell-capable agent. If
43
+ it is missing, ask the user before installing the latest official release:
44
+
45
+ ```bash
46
+ # Linux/macOS
47
+ curl -fsSL https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.sh | sh
48
+
49
+ # Windows PowerShell
50
+ powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.ps1 | iex"
51
+ ```
52
+
53
+ Keep an official build current through the release manifest:
54
+
55
+ ```bash
56
+ dcc-mcp-cli update check
57
+ dcc-mcp-cli update apply
58
+ ```
59
+
60
+ `update apply` downloads and stages the latest CLI for the next launch. It
61
+ does not update a running `dcc-mcp-server`; update that server in its own
62
+ environment.
63
+
64
+ ```bash
65
+ dcc-mcp-cli dcc-types
66
+ dcc-mcp-cli list
67
+ dcc-mcp-cli search --query "<task>" --dcc-type renderdoc
68
+ dcc-mcp-cli describe <tool-slug>
69
+ dcc-mcp-cli call <tool-slug> --json '{"key":"value"}'
70
+ ```
71
+
72
+ `dcc-types` reports release-catalog support; `list` reports live sessions. If a
73
+ tool belongs to an inactive progressive skill, call `dcc-mcp-cli load-skill <skill-name> --dcc-type renderdoc` before retrying. For post-task improvement,
74
+ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range 24h --session-id <task-id>`, then pass the bounded evidence to the
75
+ `review_skill_improvement` prompt from `dcc-mcp-skills-creator`.
76
+
77
+
31
78
  RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
32
79
 
33
80
  The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
@@ -1,5 +1,52 @@
1
1
  # dcc-mcp-renderdoc
2
2
 
3
+ <p align="center">
4
+ <img src="docs/assets/dcc-mcp-renderdoc.svg" alt="DCC-MCP · RENDERDOC" width="600">
5
+ </p>
6
+
7
+ ## Agent workflow
8
+
9
+ AI agents should use the shared gateway through `dcc-mcp-cli`; IDE users may
10
+ continue to use the MCP endpoint. Prefer typed skills and tools over raw scripts.
11
+
12
+ ### Install or update the CLI
13
+
14
+ `dcc-mcp-cli` is the preferred control path for every shell-capable agent. If
15
+ it is missing, ask the user before installing the latest official release:
16
+
17
+ ```bash
18
+ # Linux/macOS
19
+ curl -fsSL https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.sh | sh
20
+
21
+ # Windows PowerShell
22
+ powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/dcc-mcp/dcc-mcp-core/main/scripts/install-cli.ps1 | iex"
23
+ ```
24
+
25
+ Keep an official build current through the release manifest:
26
+
27
+ ```bash
28
+ dcc-mcp-cli update check
29
+ dcc-mcp-cli update apply
30
+ ```
31
+
32
+ `update apply` downloads and stages the latest CLI for the next launch. It
33
+ does not update a running `dcc-mcp-server`; update that server in its own
34
+ environment.
35
+
36
+ ```bash
37
+ dcc-mcp-cli dcc-types
38
+ dcc-mcp-cli list
39
+ dcc-mcp-cli search --query "<task>" --dcc-type renderdoc
40
+ dcc-mcp-cli describe <tool-slug>
41
+ dcc-mcp-cli call <tool-slug> --json '{"key":"value"}'
42
+ ```
43
+
44
+ `dcc-types` reports release-catalog support; `list` reports live sessions. If a
45
+ tool belongs to an inactive progressive skill, call `dcc-mcp-cli load-skill <skill-name> --dcc-type renderdoc` before retrying. For post-task improvement,
46
+ attach a stable session id with `--meta-json`, query `dcc-mcp-cli stats --range 24h --session-id <task-id>`, then pass the bounded evidence to the
47
+ `review_skill_improvement` prompt from `dcc-mcp-skills-creator`.
48
+
49
+
3
50
  RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
4
51
 
5
52
  The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dcc-mcp-renderdoc"
7
- version = "0.3.0"
7
+ version = "0.3.1"
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.1" # x-release-please-version
@@ -102,6 +102,10 @@ def capture_program(
102
102
  raise RenderDocError(f"Working directory does not exist: {cwd}")
103
103
 
104
104
  before = {path.resolve() for path in output.parent.glob("*.rdc")}
105
+ process_name = trigger_process_name or target.name
106
+ visible_before_launch = (
107
+ set(_visible_process_ids(process_name)) if trigger_after_secs is not None else set()
108
+ )
105
109
  cli_args = ["capture", "--working-dir", str(cwd), "--capture-file", str(output)]
106
110
  effective_wait_for_exit = wait_for_exit and trigger_after_secs is None
107
111
  if effective_wait_for_exit:
@@ -120,15 +124,38 @@ def capture_program(
120
124
  target_pid: Optional[int] = None
121
125
  if trigger_after_secs is not None:
122
126
  time.sleep(max(0.0, trigger_after_secs))
123
- target_pid = _wait_for_visible_process(
124
- trigger_process_name or target.name,
125
- min(capture_wait_secs, 30),
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,
126
133
  )
127
- focused = _trigger_capture_hotkey(target_pid)
128
- captures = _wait_for_captures(output.parent, before, capture_wait_secs)
129
134
  else:
130
135
  focused = False
131
136
  captures = _new_captures(output.parent, before)
137
+ injection_error: Optional[str] = None
138
+ if (
139
+ not captures
140
+ and target_pid is not None
141
+ and hook_children
142
+ and trigger_process_name is not None
143
+ ):
144
+ try:
145
+ fallback = capture_process(
146
+ target_pid,
147
+ output_template,
148
+ working_directory=str(cwd),
149
+ trigger_after_secs=0.25,
150
+ capture_wait_secs=capture_wait_secs,
151
+ api_validation=api_validation,
152
+ timeout_secs=min(timeout_secs, 60),
153
+ command=command,
154
+ )
155
+ captures = [Path(path).resolve() for path in fallback["captures"]]
156
+ focused = bool(fallback["focused_target_window"]) or focused
157
+ except RenderDocError as exc:
158
+ injection_error = str(exc)
132
159
  if not captures:
133
160
  output_detail = "\n".join(
134
161
  part.strip() for part in (result.stdout, result.stderr) if part.strip()
@@ -142,6 +169,7 @@ def capture_program(
142
169
  "No new .rdc capture appeared; ensure the target presents a frame and was hooked "
143
170
  f"before creating its graphics device. {diagnostics}. "
144
171
  f"RenderDoc output: {output_detail}"
172
+ + (f". Injection fallback: {injection_error}" if injection_error else "")
145
173
  )
146
174
  return {
147
175
  "target": str(target),
@@ -246,32 +274,47 @@ def _trigger_capture_hotkey(process_id: Optional[int] = None) -> bool:
246
274
  return True
247
275
 
248
276
  user32.EnumWindows(find_window, 0)
249
- if target:
250
- user32.SetForegroundWindow(target)
251
- time.sleep(0.15)
277
+ if not target:
278
+ return False
279
+ user32.SetForegroundWindow(target)
280
+ time.sleep(0.15)
252
281
  user32.keybd_event(0x7B, 0, 0, 0)
253
282
  user32.keybd_event(0x7B, 0, 2, 0)
254
283
  return bool(target)
255
284
 
256
285
 
257
- def _wait_for_visible_process(process_name: str, timeout_secs: int) -> Optional[int]:
258
- if sys.platform != "win32":
259
- return None
286
+ def _wait_for_triggered_capture(
287
+ directory: Path,
288
+ before: set[Path],
289
+ process_name: str,
290
+ ignored_process_ids: set[int],
291
+ timeout_secs: int,
292
+ ) -> tuple[Optional[int], bool, list[Path]]:
260
293
  deadline = time.monotonic() + timeout_secs
294
+ handled_process_ids = set(ignored_process_ids)
295
+ target_pid: Optional[int] = None
296
+ focused = False
261
297
  while time.monotonic() < deadline:
262
- process_id = _visible_process_id(process_name)
263
- if process_id is not None:
264
- return process_id
298
+ captures = _new_captures(directory, before)
299
+ if captures:
300
+ return target_pid, focused, captures
301
+ for process_id in _visible_process_ids(process_name):
302
+ if process_id in handled_process_ids:
303
+ continue
304
+ handled_process_ids.add(process_id)
305
+ target_pid = process_id
306
+ focused = _trigger_capture_hotkey(process_id) or focused
265
307
  time.sleep(0.25)
266
- return None
308
+ return target_pid, focused, _new_captures(directory, before)
267
309
 
268
310
 
269
- def _visible_process_id(process_name: str) -> Optional[int]:
311
+ def _visible_process_ids(process_name: str) -> list[int]:
270
312
  expected = Path(process_name).name.casefold()
271
- for process in _visible_processes():
272
- if process["name"].casefold() == expected:
273
- return int(process["process_id"])
274
- return None
313
+ return [
314
+ int(process["process_id"])
315
+ for process in _visible_processes()
316
+ if process["name"].casefold() == expected
317
+ ]
275
318
 
276
319
 
277
320
  def _visible_processes(limit: int = 64) -> list[dict[str, Any]]:
@@ -29,7 +29,7 @@ tools:
29
29
  wait_for_exit: {type: boolean, default: true}
30
30
  api_validation: {type: boolean, default: false}
31
31
  hook_children: {type: boolean, default: false}
32
- trigger_after_secs: {type: number, minimum: 0, maximum: 600}
32
+ trigger_after_secs: {type: number, minimum: 0, maximum: 900}
33
33
  trigger_process_name: {type: string, minLength: 1, maxLength: 260}
34
34
  capture_wait_secs: {type: integer, minimum: 1, maximum: 600, default: 30}
35
35
  timeout_secs: {type: integer, minimum: 1, maximum: 3600, default: 300}
@@ -24,6 +24,22 @@ def test_bundled_skills_and_release_workflow_exist():
24
24
  assert (root / ".github" / "workflows" / "release.yml").is_file()
25
25
 
26
26
 
27
+ def test_capture_program_accepts_ten_minute_boss_trigger():
28
+ root = Path(__file__).parents[1]
29
+ tools = (
30
+ root / "src" / "dcc_mcp_renderdoc" / "skills" / "renderdoc-capture" / "tools.yaml"
31
+ ).read_text(encoding="utf-8")
32
+ capture_program = re.search(
33
+ r" - name: capture_program(?P<body>.*?)(?=\n - name:)", tools, re.DOTALL
34
+ )
35
+ assert capture_program is not None
36
+ maximum = re.search(
37
+ r"trigger_after_secs: \{[^}]*maximum: (?P<seconds>\d+)", capture_program.group("body")
38
+ )
39
+ assert maximum is not None
40
+ assert int(maximum.group("seconds")) >= 612
41
+
42
+
27
43
  def test_runtime_version_matches_distribution_metadata():
28
44
  root = Path(__file__).parents[1]
29
45
  pyproject = (root / "pyproject.toml").read_text(encoding="utf-8")
@@ -105,16 +105,14 @@ def test_capture_program_focuses_requested_child_before_trigger(tmp_path, monkey
105
105
  lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
106
106
  )
107
107
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
108
- monkeypatch.setattr(runtime, "_wait_for_visible_process", lambda name, timeout: 77)
109
108
  monkeypatch.setattr(
110
109
  runtime,
111
- "_trigger_capture_hotkey",
112
- lambda pid: observed.setdefault("pid", pid) == 77,
113
- )
114
- monkeypatch.setattr(
115
- runtime,
116
- "_wait_for_captures",
117
- lambda directory, before, timeout: [tmp_path / "capture_frame1.rdc"],
110
+ "_wait_for_triggered_capture",
111
+ lambda directory, before, name, ignored, timeout: (
112
+ observed.setdefault("pid", 77),
113
+ True,
114
+ [tmp_path / "capture_frame1.rdc"],
115
+ ),
118
116
  )
119
117
 
120
118
  result = runtime.capture_program(
@@ -137,9 +135,11 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
137
135
  lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
138
136
  )
139
137
  monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
140
- monkeypatch.setattr(runtime, "_wait_for_visible_process", lambda name, timeout: None)
141
- monkeypatch.setattr(runtime, "_trigger_capture_hotkey", lambda pid: False)
142
- monkeypatch.setattr(runtime, "_wait_for_captures", lambda directory, before, timeout: [])
138
+ monkeypatch.setattr(
139
+ runtime,
140
+ "_wait_for_triggered_capture",
141
+ lambda directory, before, name, ignored, timeout: (None, False, []),
142
+ )
143
143
  monkeypatch.setattr(
144
144
  runtime,
145
145
  "_visible_processes",
@@ -161,6 +161,81 @@ def test_capture_program_failure_reports_missing_child_diagnostics(tmp_path, mon
161
161
  assert "RenderDoc output: Launched as ID 123" in message
162
162
 
163
163
 
164
+ def test_triggered_capture_retargets_replacement_process(tmp_path, monkeypatch):
165
+ capture = tmp_path / "capture_frame1.rdc"
166
+ process_snapshots = iter([[77], [77, 88]])
167
+ triggered = []
168
+ monkeypatch.setattr(
169
+ runtime,
170
+ "_visible_process_ids",
171
+ lambda _name: next(process_snapshots, [88]),
172
+ )
173
+ monkeypatch.setattr(
174
+ runtime,
175
+ "_trigger_capture_hotkey",
176
+ lambda process_id: not triggered.append(process_id),
177
+ )
178
+ monkeypatch.setattr(
179
+ runtime,
180
+ "_new_captures",
181
+ lambda _directory, _before: [capture] if triggered == [77, 88] else [],
182
+ )
183
+ monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
184
+
185
+ target_pid, focused, captures = runtime._wait_for_triggered_capture(
186
+ tmp_path,
187
+ set(),
188
+ "game.exe",
189
+ set(),
190
+ 1,
191
+ )
192
+
193
+ assert triggered == [77, 88]
194
+ assert target_pid == 88
195
+ assert focused is True
196
+ assert captures == [capture]
197
+
198
+
199
+ def test_capture_program_injects_visible_unhooked_child(tmp_path, monkeypatch):
200
+ target = tmp_path / "launcher.exe"
201
+ target.touch()
202
+ capture = tmp_path / "capture_frame1.rdc"
203
+ observed = {}
204
+ monkeypatch.setattr(
205
+ runtime,
206
+ "_run",
207
+ lambda arguments, **kwargs: CompletedProcess(arguments, 123, "Launched as ID 123", ""),
208
+ )
209
+ monkeypatch.setattr(runtime.time, "sleep", lambda _seconds: None)
210
+ monkeypatch.setattr(
211
+ runtime,
212
+ "_wait_for_triggered_capture",
213
+ lambda directory, before, name, ignored, timeout: (88, True, []),
214
+ )
215
+
216
+ def fake_capture_process(process_id, output_template, **kwargs):
217
+ observed.update(process_id=process_id, output_template=output_template, **kwargs)
218
+ return {
219
+ "captures": [str(capture)],
220
+ "focused_target_window": True,
221
+ "stdout": "injected",
222
+ }
223
+
224
+ monkeypatch.setattr(runtime, "capture_process", fake_capture_process)
225
+
226
+ result = runtime.capture_program(
227
+ str(target),
228
+ str(tmp_path / "capture"),
229
+ hook_children=True,
230
+ trigger_after_secs=1,
231
+ trigger_process_name="game.exe",
232
+ )
233
+
234
+ assert observed["process_id"] == 88
235
+ assert observed["trigger_after_secs"] == 0.25
236
+ assert result["captures"] == [str(capture.resolve())]
237
+
238
+
164
239
  def test_capture_process_failure_reports_injection_diagnostics(tmp_path, monkeypatch):
165
240
  monkeypatch.setattr(
166
241
  runtime,
@@ -1,2 +0,0 @@
1
- {".":"0.3.0"}
2
-
@@ -1,3 +0,0 @@
1
- """Package version."""
2
-
3
- __version__ = "0.3.0" # x-release-please-version