debugbrief 1.1.0__tar.gz → 1.2.0__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 (52) hide show
  1. {debugbrief-1.1.0/src/debugbrief.egg-info → debugbrief-1.2.0}/PKG-INFO +15 -4
  2. {debugbrief-1.1.0 → debugbrief-1.2.0}/README.md +14 -3
  3. {debugbrief-1.1.0 → debugbrief-1.2.0}/pyproject.toml +1 -1
  4. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/__init__.py +1 -1
  5. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/cli.py +51 -1
  6. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/command_runner.py +5 -0
  7. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/filters.py +27 -0
  8. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/session_manager.py +14 -0
  9. {debugbrief-1.1.0 → debugbrief-1.2.0/src/debugbrief.egg-info}/PKG-INFO +15 -4
  10. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/SOURCES.txt +2 -1
  11. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_autostart.py +1 -0
  12. debugbrief-1.2.0/tests/test_v12_features.py +154 -0
  13. {debugbrief-1.1.0 → debugbrief-1.2.0}/LICENSE +0 -0
  14. {debugbrief-1.1.0 → debugbrief-1.2.0}/setup.cfg +0 -0
  15. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/__main__.py +0 -0
  16. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/derive.py +0 -0
  17. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/doctor.py +0 -0
  18. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/git_utils.py +0 -0
  19. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/models.py +0 -0
  20. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/paths.py +0 -0
  21. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/redaction.py +0 -0
  22. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/__init__.py +0 -0
  23. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/base.py +0 -0
  24. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/handoff.py +0 -0
  25. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/incident.py +0 -0
  26. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/pr.py +0 -0
  27. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reports_index.py +0 -0
  28. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/sessions_index.py +0 -0
  29. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/utils.py +0 -0
  30. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/dependency_links.txt +0 -0
  31. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/entry_points.txt +0 -0
  32. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/requires.txt +0 -0
  33. {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/top_level.txt +0 -0
  34. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_cancel.py +0 -0
  35. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_ci_workflow.py +0 -0
  36. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_cli_ergonomics.py +0 -0
  37. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_command_runner.py +0 -0
  38. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_derive.py +0 -0
  39. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_doctor.py +0 -0
  40. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_e2e_cli.py +0 -0
  41. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_filters.py +0 -0
  42. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_git_utils.py +0 -0
  43. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_json_report.py +0 -0
  44. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_last_open.py +0 -0
  45. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_list_show.py +0 -0
  46. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_paths.py +0 -0
  47. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_redaction.py +0 -0
  48. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_reporters.py +0 -0
  49. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_run_passthrough.py +0 -0
  50. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_session_lifecycle.py +0 -0
  51. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_snapshots.py +0 -0
  52. {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbrief
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Local-first CLI that records the meaningful context of a debugging session and turns it into a useful markdown brief.
5
5
  Author: DebugBrief
6
6
  License-Expression: MIT
@@ -30,7 +30,9 @@ Dynamic: license-file
30
30
 
31
31
  # DebugBrief
32
32
 
33
- [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief)](https://pypi.org/project/debugbrief/)
33
+ [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief?cacheSeconds=3600)](https://pypi.org/project/debugbrief/)
34
+
35
+ ![A failing test streams live, the fix lands, redo passes, and the generated brief appears](https://raw.githubusercontent.com/harihkk/Debug-Brief/main/docs/demo.gif)
34
36
 
35
37
  Turn a debugging session into an honest markdown brief for a PR, a handoff, or an
36
38
  incident note.
@@ -91,11 +93,19 @@ The resulting report leads with a derived one-liner like:
91
93
  and a per-command git snapshot, then returns the command's own exit code.
92
94
  - Pass/fail comes only from the exit code. A command counts as "verified" only if
93
95
  a recognized test/build/lint/typecheck command actually exited `0`.
96
+ - Recognized runners include pytest, unittest, tox, vitest, jest, bun test,
97
+ deno test, node --test, npm/pnpm/yarn test, go test, cargo test, make
98
+ test/check, dotnet test, ctest, phpunit, mix test, swift test, rspec, and
99
+ mvn/gradle test. For custom scripts, declare the check yourself:
100
+ `debugbrief run --verify -- ./scripts/test.sh`.
94
101
  - `end` derives the report from those events: the red-to-green window, the
95
102
  reproduce/verify commands, a timeline, the observed error verbatim, and what
96
103
  was ruled out. Empty sections are omitted, never padded.
97
- - Secret-like values in captured output are replaced with `[redacted]` before
98
- anything is written to disk (best effort; `--no-redact` opts out).
104
+ - Redaction runs before anything reaches disk and catches common shapes:
105
+ sensitive `name=value` pairs, bearer and authorization headers, OpenAI/AWS/
106
+ GitHub style keys, connection-string passwords, and PEM private key blocks,
107
+ each replaced with `[redacted]`. Best effort by design; `--no-redact` opts
108
+ out per command.
99
109
 
100
110
  ## Commands
101
111
 
@@ -105,6 +115,7 @@ The resulting report leads with a derived one-liner like:
105
115
  | `note <text ...>` | Record a note (quoting optional) |
106
116
  | `run -- <command ...>` | Execute and capture a command |
107
117
  | `redo` | Re-run the last captured command |
118
+ | `preview [--mode ...]` | Print the report without ending the session |
108
119
  | `end [--mode pr\|handoff\|incident]` | Finalize and write a report (default `pr`) |
109
120
  | `cancel [--yes]` | Discard the active session, no report |
110
121
  | `status` | Show the active session |
@@ -1,6 +1,8 @@
1
1
  # DebugBrief
2
2
 
3
- [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief)](https://pypi.org/project/debugbrief/)
3
+ [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief?cacheSeconds=3600)](https://pypi.org/project/debugbrief/)
4
+
5
+ ![A failing test streams live, the fix lands, redo passes, and the generated brief appears](https://raw.githubusercontent.com/harihkk/Debug-Brief/main/docs/demo.gif)
4
6
 
5
7
  Turn a debugging session into an honest markdown brief for a PR, a handoff, or an
6
8
  incident note.
@@ -61,11 +63,19 @@ The resulting report leads with a derived one-liner like:
61
63
  and a per-command git snapshot, then returns the command's own exit code.
62
64
  - Pass/fail comes only from the exit code. A command counts as "verified" only if
63
65
  a recognized test/build/lint/typecheck command actually exited `0`.
66
+ - Recognized runners include pytest, unittest, tox, vitest, jest, bun test,
67
+ deno test, node --test, npm/pnpm/yarn test, go test, cargo test, make
68
+ test/check, dotnet test, ctest, phpunit, mix test, swift test, rspec, and
69
+ mvn/gradle test. For custom scripts, declare the check yourself:
70
+ `debugbrief run --verify -- ./scripts/test.sh`.
64
71
  - `end` derives the report from those events: the red-to-green window, the
65
72
  reproduce/verify commands, a timeline, the observed error verbatim, and what
66
73
  was ruled out. Empty sections are omitted, never padded.
67
- - Secret-like values in captured output are replaced with `[redacted]` before
68
- anything is written to disk (best effort; `--no-redact` opts out).
74
+ - Redaction runs before anything reaches disk and catches common shapes:
75
+ sensitive `name=value` pairs, bearer and authorization headers, OpenAI/AWS/
76
+ GitHub style keys, connection-string passwords, and PEM private key blocks,
77
+ each replaced with `[redacted]`. Best effort by design; `--no-redact` opts
78
+ out per command.
69
79
 
70
80
  ## Commands
71
81
 
@@ -75,6 +85,7 @@ The resulting report leads with a derived one-liner like:
75
85
  | `note <text ...>` | Record a note (quoting optional) |
76
86
  | `run -- <command ...>` | Execute and capture a command |
77
87
  | `redo` | Re-run the last captured command |
88
+ | `preview [--mode ...]` | Print the report without ending the session |
78
89
  | `end [--mode pr\|handoff\|incident]` | Finalize and write a report (default `pr`) |
79
90
  | `cancel [--yes]` | Discard the active session, no report |
80
91
  | `status` | Show the active session |
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "debugbrief"
8
- version = "1.1.0"
8
+ version = "1.2.0"
9
9
  description = "Local-first CLI that records the meaningful context of a debugging session and turns it into a useful markdown brief."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.9"
@@ -8,6 +8,6 @@ It uses only the Python standard library and native ``git``. No AI, no
8
8
  telemetry, no cloud sync, no background daemon.
9
9
  """
10
10
 
11
- __version__ = "1.1.0"
11
+ __version__ = "1.2.0"
12
12
 
13
13
  __all__ = ["__version__"]
@@ -5,7 +5,8 @@ Commands:
5
5
  debugbrief note <text ...>
6
6
  debugbrief run [--shell] [--timeout N] [--no-redact] -- <command ...>
7
7
  debugbrief run "<command>"
8
- debugbrief redo [--timeout N] [--no-redact]
8
+ debugbrief redo [--timeout N] [--no-redact] [--verify]
9
+ debugbrief preview [--mode pr|handoff|incident]
9
10
  debugbrief end [--mode pr|handoff|incident] [--format md|json|both] [--stdout]
10
11
  debugbrief cancel [--yes]
11
12
  debugbrief status
@@ -117,6 +118,15 @@ def build_parser() -> argparse.ArgumentParser:
117
118
  "redaction. Use only when you know the output is safe."
118
119
  ),
119
120
  )
121
+ p_run.add_argument(
122
+ "--verify",
123
+ action="store_true",
124
+ help=(
125
+ "Declare this command a check (custom test script, make "
126
+ "integration). It counts as verification when it exits 0; a "
127
+ "recognized test runner is classified automatically and wins."
128
+ ),
129
+ )
120
130
  p_run.add_argument(
121
131
  "command",
122
132
  nargs=argparse.REMAINDER,
@@ -147,8 +157,29 @@ def build_parser() -> argparse.ArgumentParser:
147
157
  action="store_true",
148
158
  help="Store captured output verbatim, without secret redaction.",
149
159
  )
160
+ p_redo.add_argument(
161
+ "--verify",
162
+ action="store_true",
163
+ help=(
164
+ "Declare the re-run a check (see run --verify). Inherited "
165
+ "automatically when the original run was declared with --verify."
166
+ ),
167
+ )
150
168
  p_redo.set_defaults(func=cmd_redo)
151
169
 
170
+ # preview ------------------------------------------------------------
171
+ p_preview = subparsers.add_parser(
172
+ "preview",
173
+ help="Print the report for the active session without ending it.",
174
+ )
175
+ p_preview.add_argument(
176
+ "--mode",
177
+ default="pr",
178
+ choices=VALID_MODES,
179
+ help="Report style to preview (default pr).",
180
+ )
181
+ p_preview.set_defaults(func=cmd_preview)
182
+
152
183
  # end ----------------------------------------------------------------
153
184
  p_end = subparsers.add_parser(
154
185
  "end", help="Finalize the session and write a markdown report."
@@ -373,6 +404,7 @@ def cmd_run(args: argparse.Namespace) -> int:
373
404
  use_shell=args.shell,
374
405
  timeout_seconds=args.timeout,
375
406
  redact=not args.no_redact,
407
+ force_verification=args.verify,
376
408
  )
377
409
  manager.record_command(result)
378
410
  _print_command_outcome(result, args.timeout)
@@ -433,6 +465,10 @@ def cmd_redo(args: argparse.Namespace) -> int:
433
465
  )
434
466
  return 1
435
467
 
468
+ # A redo of a command originally declared with --verify stays a declared
469
+ # check without retyping the flag; an explicit --verify also works.
470
+ inherit_verify = last.classification.tool == "custom"
471
+
436
472
  eprint(f"$ {last.command} (redo)")
437
473
  result = run_command(
438
474
  command=last.command,
@@ -440,6 +476,7 @@ def cmd_redo(args: argparse.Namespace) -> int:
440
476
  use_shell=last.used_shell,
441
477
  timeout_seconds=args.timeout,
442
478
  redact=not args.no_redact,
479
+ force_verification=args.verify or inherit_verify,
443
480
  )
444
481
  manager.record_command(result)
445
482
  _print_command_outcome(result, args.timeout)
@@ -469,6 +506,19 @@ def cmd_end(args: argparse.Namespace) -> int:
469
506
  return 0
470
507
 
471
508
 
509
+ def cmd_preview(args: argparse.Namespace) -> int:
510
+ manager = _manager()
511
+ markdown = manager.preview(args.mode)
512
+ banner = "_Preview of an active session. Run debugbrief end to finalize._"
513
+ lines = markdown.splitlines()
514
+ if lines and lines[0].startswith("# "):
515
+ rendered = lines[0] + "\n\n" + banner + "\n" + "\n".join(lines[1:]) + "\n"
516
+ else: # pragma: no cover - reports always start with a title
517
+ rendered = banner + "\n\n" + markdown
518
+ sys.stdout.write(rendered)
519
+ return 0
520
+
521
+
472
522
  def cmd_cancel(args: argparse.Namespace) -> int:
473
523
  manager = _manager()
474
524
  session = manager.load_active()
@@ -87,6 +87,7 @@ def run_command(
87
87
  stderr_limit: int = DEFAULT_STDERR_PREVIEW_LIMIT,
88
88
  redact: bool = True,
89
89
  echo: bool = True,
90
+ force_verification: bool = False,
90
91
  ) -> RunResult:
91
92
  """Run ``command`` from ``cwd`` and capture a :class:`CommandData`.
92
93
 
@@ -101,6 +102,9 @@ def run_command(
101
102
  By default captured output and the command string are passed through
102
103
  best-effort secret redaction before they are returned, so raw secrets never
103
104
  reach the session file. Pass ``redact=False`` to store the raw text.
105
+
106
+ ``force_verification`` marks an unrecognized command as a declared check
107
+ (tool ``custom``); pass/fail honesty is unaffected.
104
108
  """
105
109
  started_at = now_iso8601()
106
110
  start_monotonic = time.monotonic()
@@ -200,6 +204,7 @@ def run_command(
200
204
  exit_code=exit_code,
201
205
  timed_out=timed_out,
202
206
  errored=errored,
207
+ force_verification=force_verification,
203
208
  )
204
209
 
205
210
  stored_command = command
@@ -47,6 +47,19 @@ _TEST_PATTERNS: List[Tuple[List[str], str]] = [
47
47
  (["mvn", "test"], "maven"),
48
48
  (["gradle", "test"], "gradle"),
49
49
  (["./gradlew", "test"], "gradle"),
50
+ (["vitest"], "vitest"),
51
+ (["bun", "test"], "bun"),
52
+ (["deno", "test"], "deno"),
53
+ (["node", "--test"], "node"),
54
+ (["make", "test"], "make"),
55
+ (["make", "check"], "make"),
56
+ (["tox"], "tox"),
57
+ (["unittest"], "unittest"),
58
+ (["dotnet", "test"], "dotnet"),
59
+ (["ctest"], "ctest"),
60
+ (["phpunit"], "phpunit"),
61
+ (["mix", "test"], "mix"),
62
+ (["swift", "test"], "swift"),
50
63
  ]
51
64
 
52
65
  # (pattern_tokens, tool, category) for build/lint/typecheck detection.
@@ -120,12 +133,18 @@ def classify_command(
120
133
  exit_code: Optional[int],
121
134
  timed_out: bool = False,
122
135
  errored: bool = False,
136
+ force_verification: bool = False,
123
137
  ) -> CommandClassification:
124
138
  """Classify a command into test / verification categories.
125
139
 
126
140
  A command is verification-worthy only if it is a recognized test command
127
141
  that exited 0, or a recognized build/lint/typecheck command that exited 0.
128
142
  Pass/fail is derived strictly from the real exit code.
143
+
144
+ ``force_verification`` lets the user declare an unrecognized command (a
145
+ custom test script, ``make integration``) as a check. It applies only when
146
+ no pattern matched; a recognized runner always wins. The honesty rule is
147
+ unchanged: ``is_verification`` is True only on a real exit 0.
129
148
  """
130
149
  tokens = _tokenize(command)
131
150
  status = status_from_outcome(exit_code, timed_out, errored)
@@ -150,6 +169,14 @@ def classify_command(
150
169
  status=status,
151
170
  )
152
171
 
172
+ if force_verification:
173
+ return CommandClassification(
174
+ is_test=False,
175
+ is_verification=passed,
176
+ tool="custom",
177
+ status=status,
178
+ )
179
+
153
180
  return CommandClassification(
154
181
  is_test=False,
155
182
  is_verification=False,
@@ -263,6 +263,20 @@ class SessionManager:
263
263
  self._clear_active_pointer()
264
264
  return session
265
265
 
266
+ def preview(self, mode: str) -> str:
267
+ """Render a report for the active session without mutating anything.
268
+
269
+ Works on a deep copy (via the dict round trip), so the live session
270
+ keeps its status, timestamps, and file exactly as they are; no report
271
+ file is written. The summary is finalized on the copy only.
272
+ """
273
+ from .reporters import render_report # local import avoids a cycle
274
+
275
+ session = self.require_active("preview the report")
276
+ copy = Session.from_dict(session.to_dict())
277
+ self._finalize_summary(copy)
278
+ return render_report(copy, mode)
279
+
266
280
  def cancel(self) -> Session:
267
281
  """Discard the active session without writing a report.
268
282
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbrief
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Local-first CLI that records the meaningful context of a debugging session and turns it into a useful markdown brief.
5
5
  Author: DebugBrief
6
6
  License-Expression: MIT
@@ -30,7 +30,9 @@ Dynamic: license-file
30
30
 
31
31
  # DebugBrief
32
32
 
33
- [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief)](https://pypi.org/project/debugbrief/)
33
+ [![CI](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/debugbrief?cacheSeconds=3600)](https://pypi.org/project/debugbrief/)
34
+
35
+ ![A failing test streams live, the fix lands, redo passes, and the generated brief appears](https://raw.githubusercontent.com/harihkk/Debug-Brief/main/docs/demo.gif)
34
36
 
35
37
  Turn a debugging session into an honest markdown brief for a PR, a handoff, or an
36
38
  incident note.
@@ -91,11 +93,19 @@ The resulting report leads with a derived one-liner like:
91
93
  and a per-command git snapshot, then returns the command's own exit code.
92
94
  - Pass/fail comes only from the exit code. A command counts as "verified" only if
93
95
  a recognized test/build/lint/typecheck command actually exited `0`.
96
+ - Recognized runners include pytest, unittest, tox, vitest, jest, bun test,
97
+ deno test, node --test, npm/pnpm/yarn test, go test, cargo test, make
98
+ test/check, dotnet test, ctest, phpunit, mix test, swift test, rspec, and
99
+ mvn/gradle test. For custom scripts, declare the check yourself:
100
+ `debugbrief run --verify -- ./scripts/test.sh`.
94
101
  - `end` derives the report from those events: the red-to-green window, the
95
102
  reproduce/verify commands, a timeline, the observed error verbatim, and what
96
103
  was ruled out. Empty sections are omitted, never padded.
97
- - Secret-like values in captured output are replaced with `[redacted]` before
98
- anything is written to disk (best effort; `--no-redact` opts out).
104
+ - Redaction runs before anything reaches disk and catches common shapes:
105
+ sensitive `name=value` pairs, bearer and authorization headers, OpenAI/AWS/
106
+ GitHub style keys, connection-string passwords, and PEM private key blocks,
107
+ each replaced with `[redacted]`. Best effort by design; `--no-redact` opts
108
+ out per command.
99
109
 
100
110
  ## Commands
101
111
 
@@ -105,6 +115,7 @@ The resulting report leads with a derived one-liner like:
105
115
  | `note <text ...>` | Record a note (quoting optional) |
106
116
  | `run -- <command ...>` | Execute and capture a command |
107
117
  | `redo` | Re-run the last captured command |
118
+ | `preview [--mode ...]` | Print the report without ending the session |
108
119
  | `end [--mode pr\|handoff\|incident]` | Finalize and write a report (default `pr`) |
109
120
  | `cancel [--yes]` | Discard the active session, no report |
110
121
  | `status` | Show the active session |
@@ -46,4 +46,5 @@ tests/test_reporters.py
46
46
  tests/test_run_passthrough.py
47
47
  tests/test_session_lifecycle.py
48
48
  tests/test_snapshots.py
49
- tests/test_utils.py
49
+ tests/test_utils.py
50
+ tests/test_v12_features.py
@@ -48,6 +48,7 @@ def test_run_autostarts_session(paths, capsys):
48
48
  shell=False,
49
49
  timeout=30,
50
50
  no_redact=False,
51
+ verify=False,
51
52
  )
52
53
  )
53
54
  assert rc == 0
@@ -0,0 +1,154 @@
1
+ """Tests for the expanded runner table, --verify, and preview."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import pytest
8
+
9
+ from debugbrief import cli, filters
10
+ from debugbrief.paths import ProjectPaths
11
+ from debugbrief.session_manager import SessionManager
12
+
13
+ PY = sys.executable
14
+
15
+
16
+ @pytest.fixture
17
+ def paths(tmp_path):
18
+ return ProjectPaths(project_root=tmp_path, is_git_repo=False, repo_root=None)
19
+
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def _patch_resolve(monkeypatch, paths):
23
+ monkeypatch.setattr(cli, "resolve_project_paths", lambda: paths)
24
+ return paths
25
+
26
+
27
+ # Expanded runner table -------------------------------------------------------
28
+ @pytest.mark.parametrize(
29
+ ("command", "tool"),
30
+ [
31
+ ("vitest run", "vitest"),
32
+ ("npx vitest", "vitest"),
33
+ ("bun test", "bun"),
34
+ ("deno test --allow-read", "deno"),
35
+ ("node --test tests/", "node"),
36
+ ("make test", "make"),
37
+ ("make check", "make"),
38
+ ("tox -e py311", "tox"),
39
+ ("python -m unittest discover", "unittest"),
40
+ ("dotnet test", "dotnet"),
41
+ ("ctest --output-on-failure", "ctest"),
42
+ ("phpunit tests/", "phpunit"),
43
+ ("mix test", "mix"),
44
+ ("swift test", "swift"),
45
+ ],
46
+ )
47
+ def test_new_runners_classified(command, tool):
48
+ passing = filters.classify_command(command, exit_code=0)
49
+ assert passing.is_test is True, command
50
+ assert passing.tool == tool, command
51
+ assert passing.is_verification is True, command
52
+
53
+ failing = filters.classify_command(command, exit_code=1)
54
+ assert failing.is_test is True, command
55
+ assert failing.tool == tool, command
56
+ assert failing.is_verification is False, command
57
+
58
+
59
+ @pytest.mark.parametrize("command", ["make build", "bun install", "deno run app.ts"])
60
+ def test_non_test_invocations_not_classified(command):
61
+ cls = filters.classify_command(command, exit_code=0)
62
+ assert cls.is_test is False, command
63
+ assert cls.tool is None or cls.tool not in ("make", "bun", "deno"), command
64
+
65
+
66
+ # --verify --------------------------------------------------------------------
67
+ def test_verify_marks_custom_command_as_check():
68
+ cls = filters.classify_command(
69
+ "./scripts/integration.sh", exit_code=0, force_verification=True
70
+ )
71
+ assert cls.tool == "custom"
72
+ assert cls.is_test is False
73
+ assert cls.is_verification is True
74
+
75
+
76
+ def test_verify_failure_stays_honest():
77
+ cls = filters.classify_command(
78
+ "./scripts/integration.sh", exit_code=1, force_verification=True
79
+ )
80
+ assert cls.tool == "custom"
81
+ assert cls.is_verification is False
82
+
83
+
84
+ def test_verify_is_noop_on_recognized_runner():
85
+ cls = filters.classify_command("pytest -q", exit_code=0, force_verification=True)
86
+ assert cls.tool == "pytest"
87
+ assert cls.is_test is True
88
+
89
+
90
+ def test_verify_enables_red_to_green_for_custom_check(paths):
91
+ # A custom script fails, then passes: with --verify both runs are
92
+ # verification candidates, so the report derives reproduce/verify lines.
93
+ script = paths.project_root / "check.sh"
94
+ script.write_text("#!/bin/sh\nexit $(cat flag)\n", encoding="utf-8")
95
+ script.chmod(0o755)
96
+ flag = paths.project_root / "flag"
97
+
98
+ flag.write_text("1", encoding="utf-8")
99
+ assert cli.main(["run", "--verify", "--", "./check.sh"]) == 1
100
+ flag.write_text("0", encoding="utf-8")
101
+ assert cli.main(["run", "--verify", "--", "./check.sh"]) == 0
102
+
103
+ report = SessionManager(paths).preview("pr")
104
+ assert "Reproduce (failed): `./check.sh`" in report
105
+ assert "Verify (passed): `./check.sh`" in report
106
+
107
+
108
+ def test_redo_inherits_verify(paths):
109
+ (paths.project_root / "ok.sh").write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
110
+ (paths.project_root / "ok.sh").chmod(0o755)
111
+ assert cli.main(["run", "--verify", "--", "./ok.sh"]) == 0
112
+ assert cli.main(["redo"]) == 0
113
+
114
+ events = SessionManager(paths).load_active().command_events()
115
+ assert len(events) == 2
116
+ assert events[1].data["classification"]["tool"] == "custom"
117
+ assert events[1].data["classification"]["is_verification"] is True
118
+
119
+
120
+ # preview ---------------------------------------------------------------------
121
+ def test_preview_renders_without_mutating(paths, capsys):
122
+ manager = SessionManager(paths)
123
+ session = manager.start("preview me")
124
+ manager.add_note("an observation")
125
+ before = paths.session_file(session.session_id).read_bytes()
126
+ capsys.readouterr()
127
+
128
+ rc = cli.main(["preview"])
129
+ assert rc == 0
130
+ out = capsys.readouterr().out
131
+ assert out.startswith("# preview me")
132
+ assert "Preview of an active session" in out
133
+ assert "an observation" in out
134
+
135
+ # Nothing changed on disk: session byte-identical, no reports written.
136
+ assert paths.session_file(session.session_id).read_bytes() == before
137
+ assert manager.load_active().status == "ACTIVE"
138
+ assert not paths.reports_dir.exists() or not list(paths.reports_dir.glob("*"))
139
+
140
+
141
+ def test_preview_mid_session_with_commands(paths, capsys):
142
+ cli.main(["run", "--", PY, "-c", "print(42)"])
143
+ capsys.readouterr()
144
+ rc = cli.main(["preview", "--mode", "incident"])
145
+ assert rc == 0
146
+ out = capsys.readouterr().out
147
+ assert "Preview of an active session" in out
148
+ assert "print(42)" in out
149
+
150
+
151
+ def test_preview_without_session_errors(paths, capsys):
152
+ rc = cli.main(["preview"])
153
+ assert rc == 1
154
+ assert "No active DebugBrief session" in capsys.readouterr().err
File without changes
File without changes