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.
- {debugbrief-1.1.0/src/debugbrief.egg-info → debugbrief-1.2.0}/PKG-INFO +15 -4
- {debugbrief-1.1.0 → debugbrief-1.2.0}/README.md +14 -3
- {debugbrief-1.1.0 → debugbrief-1.2.0}/pyproject.toml +1 -1
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/__init__.py +1 -1
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/cli.py +51 -1
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/command_runner.py +5 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/filters.py +27 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/session_manager.py +14 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0/src/debugbrief.egg-info}/PKG-INFO +15 -4
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/SOURCES.txt +2 -1
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_autostart.py +1 -0
- debugbrief-1.2.0/tests/test_v12_features.py +154 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/LICENSE +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/setup.cfg +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/__main__.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/derive.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/doctor.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/git_utils.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/models.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/paths.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/redaction.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/__init__.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/base.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/handoff.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/incident.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reporters/pr.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/reports_index.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/sessions_index.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief/utils.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/dependency_links.txt +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/entry_points.txt +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/requires.txt +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/src/debugbrief.egg-info/top_level.txt +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_cancel.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_ci_workflow.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_cli_ergonomics.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_command_runner.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_derive.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_doctor.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_e2e_cli.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_filters.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_git_utils.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_json_report.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_last_open.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_list_show.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_paths.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_redaction.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_reporters.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_run_passthrough.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_session_lifecycle.py +0 -0
- {debugbrief-1.1.0 → debugbrief-1.2.0}/tests/test_snapshots.py +0 -0
- {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.
|
|
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
|
-
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
33
|
+
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
34
|
+
|
|
35
|
+

|
|
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
|
-
-
|
|
98
|
-
|
|
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
|
-
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
3
|
+
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
4
|
+
|
|
5
|
+

|
|
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
|
-
-
|
|
68
|
-
|
|
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.
|
|
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"
|
|
@@ -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.
|
|
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
|
-
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
33
|
+
[](https://github.com/harihkk/Debug-Brief/actions/workflows/ci.yml) [](https://pypi.org/project/debugbrief/)
|
|
34
|
+
|
|
35
|
+

|
|
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
|
-
-
|
|
98
|
-
|
|
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 |
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|