@laitszkin/apollo-toolkit 2.11.2 → 2.11.4

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. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +3 -2
  4. package/analyse-app-logs/README.md +12 -0
  5. package/analyse-app-logs/SKILL.md +6 -1
  6. package/analyse-app-logs/agents/openai.yaml +1 -1
  7. package/analyse-app-logs/scripts/filter_logs_by_time.py +64 -0
  8. package/analyse-app-logs/scripts/log_cli_utils.py +112 -0
  9. package/analyse-app-logs/scripts/search_logs.py +137 -0
  10. package/analyse-app-logs/tests/test_filter_logs_by_time.py +95 -0
  11. package/analyse-app-logs/tests/test_search_logs.py +100 -0
  12. package/commit-and-push/SKILL.md +20 -10
  13. package/maintain-skill-catalog/SKILL.md +3 -1
  14. package/open-github-issue/README.md +48 -6
  15. package/open-github-issue/SKILL.md +80 -5
  16. package/open-github-issue/agents/openai.yaml +2 -2
  17. package/open-github-issue/scripts/open_github_issue.py +174 -1
  18. package/open-github-issue/tests/test_open_github_issue.py +79 -0
  19. package/package.json +1 -1
  20. package/production-sim-debug/LICENSE +21 -0
  21. package/production-sim-debug/README.md +91 -0
  22. package/production-sim-debug/SKILL.md +131 -0
  23. package/production-sim-debug/agents/openai.yaml +4 -0
  24. package/read-github-issue/SKILL.md +99 -0
  25. package/read-github-issue/agents/openai.yaml +4 -0
  26. package/{fix-github-issues/scripts/list_issues.py → read-github-issue/scripts/find_issues.py} +1 -1
  27. package/read-github-issue/scripts/read_issue.py +108 -0
  28. package/{fix-github-issues/tests/test_list_issues.py → read-github-issue/tests/test_find_issues.py} +3 -3
  29. package/read-github-issue/tests/test_read_issue.py +109 -0
  30. package/ship-github-issue-fix/SKILL.md +65 -0
  31. package/ship-github-issue-fix/agents/openai.yaml +4 -0
  32. package/version-release/SKILL.md +4 -1
  33. package/fix-github-issues/SKILL.md +0 -105
  34. package/fix-github-issues/agents/openai.yaml +0 -4
package/AGENTS.md CHANGED
@@ -13,7 +13,7 @@ This repository enables users to install and run a curated set of reusable agent
13
13
 
14
14
  - Users can align project documentation with the current codebase.
15
15
  - Users can consolidate completed project specs into a standardized README and categorized project documentation set, then archive the consumed planning files.
16
- - Users can investigate application logs and produce evidence-backed root-cause findings.
16
+ - Users can investigate application logs, slice them to precise time windows, search by keyword or regex, and produce evidence-backed root-cause findings.
17
17
  - Users can answer repository-backed questions with additional web research when needed.
18
18
  - Users can commit and push local changes without performing version or release work.
19
19
  - Users can manage Codex user-preference memory by reviewing the last 24 hours of chats, storing categorized memory documents under `~/.codex/memory`, and syncing a memory index into `~/.codex/AGENTS.md`.
@@ -28,7 +28,8 @@ This repository enables users to install and run a curated set of reusable agent
28
28
  - Users can extend existing features in a brownfield codebase with required tests and approvals.
29
29
  - Users can propose product features from an existing codebase and publish accepted proposals.
30
30
  - Users can discover reproducible edge-case risks and report prioritized hardening gaps.
31
- - Users can fix remote GitHub issues locally and submit the resulting pull requests.
31
+ - Users can read, filter, and inspect remote GitHub issues before planning follow-up work.
32
+ - Users can resolve a GitHub issue end-to-end and push the fix directly to a requested branch without opening a PR.
32
33
  - Users can run evidence-first application security audits focused on confirmed vulnerabilities.
33
34
  - Users can learn new or improved skills from recent Codex conversation history.
34
35
  - Users can audit and maintain the skill catalog itself, including dependency classification and shared-skill extraction decisions.
@@ -40,6 +41,7 @@ This repository enables users to install and run a curated set of reusable agent
40
41
  - Users can prepare and open open-source pull requests from existing changes.
41
42
  - Users can generate storyboard image sets from chapters, novels, articles, or scripts.
42
43
  - Users can configure OpenClaw from official documentation, including `~/.openclaw/openclaw.json`, skills loading, SecretRefs, CLI edits, and validation or repair workflows.
44
+ - Users can investigate production or local simulation runs, calibrate reusable presets, and fix toolchain realism gaps between harness behavior and expected on-chain behavior.
43
45
  - Users can record multi-account spending and balance changes in monthly Excel ledgers with summary analytics and charts.
44
46
  - Users can review the current git change set from an unbiased reviewer perspective to find abstraction opportunities and simplification candidates.
45
47
  - Users can process GitHub pull request review comments and resolve addressed threads.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to this repository are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v2.11.4] - 2026-03-27
8
+
9
+ ### Added
10
+ - Add `production-sim-debug` for investigating production or local simulation runs, separating harness realism gaps from runtime bugs, and validating fixes by rerunning the same bounded scenario.
11
+ - Add `ship-github-issue-fix` for taking a remote GitHub issue through implementation and direct push to a requested branch without opening a PR or performing release work.
12
+
13
+ ### Changed
14
+ - Update `read-github-issue` to prefer bundled issue scripts while falling back to raw `gh issue list` and `gh issue view` commands when repository-specific helpers are missing or fail.
15
+ - Strengthen `commit-and-push` and `version-release` so sequential git mutations must verify the remote branch tip and release tag before reporting success or publishing a release.
16
+ - Refresh repository capability docs and skill inventory to include direct issue-shipping and production simulation debugging workflows.
17
+
18
+ ## [v2.11.3] - 2026-03-24
19
+
20
+ ### Added
21
+ - Add bundled `analyse-app-logs` scripts for filtering logs by bounded time windows and searching by keyword or regex, with focused tests for both helpers.
22
+ - Add `read-github-issue` as a dedicated GitHub issue discovery skill with bundled scripts for finding issue candidates and reading a specific issue with comments.
23
+
24
+ ### Changed
25
+ - Expand `open-github-issue` to support structured `performance`, `security`, `docs`, and `observability` issue categories in addition to `problem` and `feature`.
26
+ - Refocus the former `fix-github-issues` workflow into read-only GitHub issue discovery and inspection guidance instead of a hardcoded fixing workflow.
27
+ - Update repository capability docs and agent prompts to reflect the new GitHub issue-reading and log-search workflows.
28
+
7
29
  ## [v2.11.2] - 2026-03-23
8
30
 
9
31
  ### Changed
package/README.md CHANGED
@@ -19,7 +19,7 @@ A curated skill catalog for Codex, OpenClaw, and Trae with a managed installer t
19
19
  - enhance-existing-features
20
20
  - feature-propose
21
21
  - financial-research
22
- - fix-github-issues
22
+ - read-github-issue
23
23
  - generate-spec
24
24
  - harden-app-security
25
25
  - improve-observability
@@ -35,6 +35,7 @@ A curated skill catalog for Codex, OpenClaw, and Trae with a managed installer t
35
35
  - open-source-pr-workflow
36
36
  - openai-text-to-image-storyboard
37
37
  - openclaw-configuration
38
+ - production-sim-debug
38
39
  - record-spending
39
40
  - resolve-review-comments
40
41
  - review-change-set
@@ -142,7 +143,7 @@ Compatibility note:
142
143
 
143
144
  - `generate-spec` is a local skill used by `develop-new-features` and `enhance-existing-features`.
144
145
  - `maintain-skill-catalog` can conditionally use `find-skills`, but its install source is not verified in this repository, so it is intentionally omitted from the table.
145
- - `fix-github-issues` accepts `open-source-pr-workflow` or an environment alias named `open-pr-workflow`. Apollo Toolkit already vendors `open-source-pr-workflow`, so `open-pr-workflow` is not a required external dependency here.
146
+ - `read-github-issue` uses GitHub CLI (`gh`) directly for remote issue discovery and inspection, so it does not add any extra skill dependency.
146
147
 
147
148
  ## Release publishing
148
149
 
@@ -11,6 +11,7 @@ This skill helps agents analyze logs end-to-end, correlate runtime signals with
11
11
  - A strict evidence standard (log lines + code correlation + impact + confidence).
12
12
  - A checklist to avoid false conclusions.
13
13
  - A pattern catalog for common operational failures (timeouts, retry storms, auth errors, resource pressure, schema mismatch, race conditions, and dependency outages).
14
+ - Bundled CLI helpers for filtering logs to a specific time window and searching by keyword or regex.
14
15
  - GitHub issue publication via the `open-github-issue` dependency skill.
15
16
  - Issue language selection delegated to `open-github-issue` based on the target repository README: Chinese README -> localized issue body, otherwise English issue body.
16
17
 
@@ -20,6 +21,9 @@ This skill helps agents analyze logs end-to-end, correlate runtime signals with
20
21
  - `agents/openai.yaml`: Agent interface metadata and default prompt.
21
22
  - `references/investigation-checklist.md`: Investigation validation checklist.
22
23
  - `references/log-signal-patterns.md`: Log signal pattern reference.
24
+ - `scripts/filter_logs_by_time.py`: Time-window log filtering helper.
25
+ - `scripts/search_logs.py`: Keyword / regex search helper with optional context.
26
+ - `scripts/log_cli_utils.py`: Shared timestamp parsing utilities.
23
27
  - Dependency skill: `open-github-issue` for deterministic issue publishing.
24
28
 
25
29
  ## Installation
@@ -65,6 +69,14 @@ Best results come from including:
65
69
 
66
70
  When the time window is not explicitly provided, the skill should first derive a bounded analysis window from a recent concrete event, such as the last container restart, and inspect only that slice before widening the search.
67
71
 
72
+ Useful bundled commands:
73
+
74
+ ```bash
75
+ python3 scripts/filter_logs_by_time.py app.log --start "2026-03-24T10:00:00Z" --end "2026-03-24T10:30:00Z"
76
+ python3 scripts/search_logs.py app.log --keyword timeout --keyword payment --mode all --after-context 3
77
+ python3 scripts/search_logs.py app.log --regex "request_id=ab12.*ERROR" --start "2026-03-24T10:00:00Z" --end "2026-03-24T10:15:00Z"
78
+ ```
79
+
68
80
  ## Example
69
81
 
70
82
  ### Input prompt
@@ -15,7 +15,7 @@ description: Comprehensive application log investigation workflow that reads log
15
15
  ## Standards
16
16
 
17
17
  - Evidence: Use a bounded investigation window and correlate log lines with code, runtime context, and concrete identifiers.
18
- - Execution: Scope the incident, build a timeline, validate candidate issues, then prioritize and optionally publish them.
18
+ - Execution: Scope the incident, use the bundled scripts to cut logs down by time window or search terms, build a timeline, validate candidate issues, then prioritize and optionally publish them.
19
19
  - Quality: Separate confirmed issues from hypotheses and include time-window, log, code, impact, and confidence evidence for each report.
20
20
  - Output: Return incident summary, confirmed issues, hypotheses, monitoring improvements, and publication status.
21
21
 
@@ -38,9 +38,11 @@ Use this skill to analyze application logs systematically with the codebase and
38
38
  - If the user does not provide a trustworthy window, derive one from a concrete runtime boundary first, such as the last container restart, pod recreation, deploy start, worker boot, or first failure after a known healthy state.
39
39
  - Prefer analyzing logs only inside that bounded window first (for example, from the last restart until now) to avoid stale logs polluting the diagnosis; widen the window only when the bounded slice cannot explain the symptom.
40
40
  - Identify relevant identifiers (trace ID, request ID, user ID, job ID, tx hash).
41
+ - Use `scripts/filter_logs_by_time.py` first when the raw log set is large and the incident window can be bounded.
41
42
  2. Build a timeline from logs
42
43
  - Extract key events in chronological order within the chosen window: deploys, config changes, warnings, errors, retries, and recoveries.
43
44
  - Group repeated symptoms by signature (error type, message prefix, stack frame, endpoint).
45
+ - Use `scripts/search_logs.py` to narrow by error signature, IDs, endpoint names, or repeated keywords before summarizing the timeline.
44
46
  3. Correlate across context
45
47
  - Link related log lines using identifiers and timestamps.
46
48
  - Map stack traces and log messages to exact code locations.
@@ -118,4 +120,7 @@ Use this structure in responses:
118
120
 
119
121
  - `references/investigation-checklist.md`: Step-by-step checklist for evidence-driven log investigations.
120
122
  - `references/log-signal-patterns.md`: Common log signatures, likely causes, validation hints, and false-positive guards.
123
+ - `scripts/filter_logs_by_time.py`: Filter raw logs to a bounded incident window from files or stdin.
124
+ - `scripts/search_logs.py`: Search logs by keyword or regex with optional time-window filtering and context lines.
125
+ - `scripts/log_cli_utils.py`: Shared timestamp parsing and stdin/file iteration utilities for the bundled log scripts.
121
126
  - Dependency skill: `open-github-issue` for deterministic issue publishing with auth fallback and README language detection.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Analyse App Logs"
3
3
  short_description: "Analyze logs with code context to detect issues"
4
- default_prompt: "Use $analyse-app-logs to inspect application logs with code context, first anchor the investigation to a bounded recent time window such as the last container restart or deploy, identify evidence-backed issues, suggest concrete remediation steps, and delegate confirmed GitHub issue publication to $open-github-issue."
4
+ default_prompt: "Use $analyse-app-logs to inspect application logs with code context, first anchor the investigation to a bounded recent time window such as the last container restart or deploy, use the bundled log-filter and log-search scripts to narrow the evidence set precisely, identify evidence-backed issues, suggest concrete remediation steps, and delegate confirmed GitHub issue publication to $open-github-issue."
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from log_cli_utils import ensure_timezone, extract_timestamp, in_window, iter_input_lines, parse_cli_timestamp
9
+
10
+
11
+ def parse_args() -> argparse.Namespace:
12
+ parser = argparse.ArgumentParser(
13
+ description="Filter log lines by timestamp window from files or stdin."
14
+ )
15
+ parser.add_argument("paths", nargs="*", help="Log file paths. Reads stdin when omitted.")
16
+ parser.add_argument("--start", help="Inclusive start timestamp.")
17
+ parser.add_argument("--end", help="Inclusive end timestamp.")
18
+ parser.add_argument(
19
+ "--assume-timezone",
20
+ default="UTC",
21
+ help="Timezone for naive timestamps and --start/--end values. Default: UTC.",
22
+ )
23
+ parser.add_argument(
24
+ "--keep-undated",
25
+ action="store_true",
26
+ help="Keep lines without a parseable timestamp.",
27
+ )
28
+ parser.add_argument(
29
+ "--count-only",
30
+ action="store_true",
31
+ help="Print only the count of matching lines.",
32
+ )
33
+ return parser.parse_args()
34
+
35
+
36
+ def main() -> int:
37
+ args = parse_args()
38
+ assume_timezone = ensure_timezone(args.assume_timezone)
39
+ start = parse_cli_timestamp(args.start, assume_timezone) if args.start else None
40
+ end = parse_cli_timestamp(args.end, assume_timezone) if args.end else None
41
+
42
+ if start and end and start > end:
43
+ print("Error: --start must be earlier than or equal to --end.", file=sys.stderr)
44
+ return 1
45
+
46
+ matches = 0
47
+ for line in iter_input_lines(args.paths):
48
+ timestamp = extract_timestamp(line, assume_timezone)
49
+ if timestamp is None and not args.keep_undated:
50
+ continue
51
+ if timestamp is not None and not in_window(timestamp, start, end):
52
+ continue
53
+
54
+ matches += 1
55
+ if not args.count_only:
56
+ print(line)
57
+
58
+ if args.count_only:
59
+ print(matches)
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ sys.exit(main())
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ from datetime import datetime, timedelta, timezone
8
+ from pathlib import Path
9
+ from typing import Iterator, Sequence
10
+
11
+
12
+ TIMESTAMP_PATTERN = re.compile(
13
+ r"(?P<timestamp>"
14
+ r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:\d{2})?"
15
+ r")"
16
+ )
17
+
18
+ TIMESTAMP_FORMATS = (
19
+ "%Y-%m-%dT%H:%M:%S.%f%z",
20
+ "%Y-%m-%dT%H:%M:%S%z",
21
+ "%Y-%m-%d %H:%M:%S.%f%z",
22
+ "%Y-%m-%d %H:%M:%S%z",
23
+ "%Y-%m-%dT%H:%M:%S.%f",
24
+ "%Y-%m-%dT%H:%M:%S",
25
+ "%Y-%m-%d %H:%M:%S.%f",
26
+ "%Y-%m-%d %H:%M:%S",
27
+ )
28
+
29
+
30
+ def normalize_timestamp(raw: str) -> str:
31
+ value = raw.strip().replace(",", ".")
32
+ if value.endswith("Z"):
33
+ return value[:-1] + "+00:00"
34
+ return value
35
+
36
+
37
+ def parse_cli_timestamp(raw: str, assume_timezone: timezone) -> datetime:
38
+ normalized = normalize_timestamp(raw)
39
+ for fmt in TIMESTAMP_FORMATS:
40
+ try:
41
+ parsed = datetime.strptime(normalized, fmt)
42
+ except ValueError:
43
+ continue
44
+ if parsed.tzinfo is None:
45
+ return parsed.replace(tzinfo=assume_timezone)
46
+ return parsed
47
+ raise argparse.ArgumentTypeError(f"invalid timestamp: {raw}")
48
+
49
+
50
+ def extract_timestamp(line: str, assume_timezone: timezone) -> datetime | None:
51
+ match = TIMESTAMP_PATTERN.search(line)
52
+ if not match:
53
+ return None
54
+ try:
55
+ return parse_cli_timestamp(match.group("timestamp"), assume_timezone)
56
+ except argparse.ArgumentTypeError:
57
+ return None
58
+
59
+
60
+ def build_timezone(raw: str) -> timezone:
61
+ if raw.upper() == "UTC":
62
+ return timezone.utc
63
+
64
+ match = re.fullmatch(r"([+-])(\d{2}):(\d{2})", raw)
65
+ if not match:
66
+ raise argparse.ArgumentTypeError("timezone must be UTC or ±HH:MM")
67
+
68
+ sign, hours, minutes = match.groups()
69
+ total_minutes = int(hours) * 60 + int(minutes)
70
+ if sign == "-":
71
+ total_minutes *= -1
72
+ return timezone.utc if total_minutes == 0 else timezone(timedelta(minutes=total_minutes))
73
+
74
+
75
+ def iter_input_lines(paths: Sequence[str]) -> Iterator[str]:
76
+ if not paths:
77
+ import sys
78
+
79
+ for line in sys.stdin:
80
+ yield line.rstrip("\n")
81
+ return
82
+
83
+ for raw_path in paths:
84
+ if raw_path == "-":
85
+ import sys
86
+
87
+ for line in sys.stdin:
88
+ yield line.rstrip("\n")
89
+ continue
90
+
91
+ path = Path(raw_path)
92
+ with path.open("r", encoding="utf-8") as handle:
93
+ for line in handle:
94
+ yield line.rstrip("\n")
95
+
96
+
97
+ def in_window(
98
+ timestamp: datetime | None,
99
+ start: datetime | None,
100
+ end: datetime | None,
101
+ ) -> bool:
102
+ if timestamp is None:
103
+ return False
104
+ if start and timestamp < start:
105
+ return False
106
+ if end and timestamp > end:
107
+ return False
108
+ return True
109
+
110
+
111
+ def ensure_timezone(value: str) -> timezone:
112
+ return build_timezone(value)
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ import sys
8
+ from collections import deque
9
+ from typing import Callable
10
+
11
+ from log_cli_utils import ensure_timezone, extract_timestamp, in_window, iter_input_lines, parse_cli_timestamp
12
+
13
+
14
+ def parse_args() -> argparse.Namespace:
15
+ parser = argparse.ArgumentParser(
16
+ description="Search log lines by keyword or regex, with optional time filtering."
17
+ )
18
+ parser.add_argument("paths", nargs="*", help="Log file paths. Reads stdin when omitted.")
19
+ parser.add_argument(
20
+ "--keyword",
21
+ action="append",
22
+ default=[],
23
+ help="Keyword to match. Repeat for multiple values.",
24
+ )
25
+ parser.add_argument(
26
+ "--regex",
27
+ action="append",
28
+ default=[],
29
+ help="Regular expression to match. Repeat for multiple values.",
30
+ )
31
+ parser.add_argument(
32
+ "--mode",
33
+ choices=["any", "all"],
34
+ default="any",
35
+ help="Require any or all patterns to match. Default: any.",
36
+ )
37
+ parser.add_argument(
38
+ "--ignore-case",
39
+ action="store_true",
40
+ help="Case-insensitive matching for keywords and regex.",
41
+ )
42
+ parser.add_argument("--start", help="Inclusive start timestamp.")
43
+ parser.add_argument("--end", help="Inclusive end timestamp.")
44
+ parser.add_argument(
45
+ "--assume-timezone",
46
+ default="UTC",
47
+ help="Timezone for naive timestamps and --start/--end values. Default: UTC.",
48
+ )
49
+ parser.add_argument(
50
+ "--before-context",
51
+ type=int,
52
+ default=0,
53
+ help="Print N lines of context before each match.",
54
+ )
55
+ parser.add_argument(
56
+ "--after-context",
57
+ type=int,
58
+ default=0,
59
+ help="Print N lines of context after each match.",
60
+ )
61
+ parser.add_argument(
62
+ "--count-only",
63
+ action="store_true",
64
+ help="Print only the number of matching lines.",
65
+ )
66
+ return parser.parse_args()
67
+
68
+
69
+ def build_matchers(args: argparse.Namespace) -> list[Callable[[str], bool]]:
70
+ flags = re.IGNORECASE if args.ignore_case else 0
71
+ matchers: list[Callable[[str], bool]] = []
72
+
73
+ for keyword in args.keyword:
74
+ needle = keyword.lower() if args.ignore_case else keyword
75
+
76
+ def match_keyword(line: str, needle: str = needle) -> bool:
77
+ haystack = line.lower() if args.ignore_case else line
78
+ return needle in haystack
79
+
80
+ matchers.append(match_keyword)
81
+
82
+ for pattern in args.regex:
83
+ compiled = re.compile(pattern, flags)
84
+ matchers.append(lambda line, compiled=compiled: bool(compiled.search(line)))
85
+
86
+ return matchers
87
+
88
+
89
+ def line_matches(line: str, matchers: list[Callable[[str], bool]], mode: str) -> bool:
90
+ if not matchers:
91
+ return True
92
+ results = [matcher(line) for matcher in matchers]
93
+ return any(results) if mode == "any" else all(results)
94
+
95
+
96
+ def main() -> int:
97
+ args = parse_args()
98
+ assume_timezone = ensure_timezone(args.assume_timezone)
99
+ start = parse_cli_timestamp(args.start, assume_timezone) if args.start else None
100
+ end = parse_cli_timestamp(args.end, assume_timezone) if args.end else None
101
+ if start and end and start > end:
102
+ print("Error: --start must be earlier than or equal to --end.", file=sys.stderr)
103
+ return 1
104
+
105
+ matchers = build_matchers(args)
106
+ matches = 0
107
+ before_buffer: deque[str] = deque(maxlen=args.before_context)
108
+ after_remaining = 0
109
+
110
+ for line in iter_input_lines(args.paths):
111
+ timestamp = extract_timestamp(line, assume_timezone)
112
+ if (start or end) and not in_window(timestamp, start, end):
113
+ before_buffer.append(line)
114
+ continue
115
+
116
+ is_match = line_matches(line, matchers, args.mode)
117
+
118
+ if is_match:
119
+ matches += 1
120
+ if not args.count_only:
121
+ while before_buffer:
122
+ print(before_buffer.popleft())
123
+ print(line)
124
+ after_remaining = args.after_context
125
+ elif after_remaining > 0 and not args.count_only:
126
+ print(line)
127
+ after_remaining -= 1
128
+
129
+ before_buffer.append(line)
130
+
131
+ if args.count_only:
132
+ print(matches)
133
+ return 0
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import tempfile
8
+ import unittest
9
+ from argparse import Namespace
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+
14
+ SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "filter_logs_by_time.py"
15
+ SCRIPT_DIR = SCRIPT_PATH.parent
16
+ if str(SCRIPT_DIR) not in __import__("sys").path:
17
+ __import__("sys").path.insert(0, str(SCRIPT_DIR))
18
+ SPEC = importlib.util.spec_from_file_location("filter_logs_by_time", SCRIPT_PATH)
19
+ MODULE = importlib.util.module_from_spec(SPEC)
20
+ SPEC.loader.exec_module(MODULE)
21
+
22
+
23
+ class FilterLogsByTimeTests(unittest.TestCase):
24
+ def test_filters_lines_inside_window(self) -> None:
25
+ with tempfile.NamedTemporaryFile("w+", encoding="utf-8", delete=False) as handle:
26
+ handle.write(
27
+ "2026-03-24T10:00:00Z INFO boot\n"
28
+ "2026-03-24T10:05:00Z ERROR failed\n"
29
+ "2026-03-24T10:10:00Z INFO recovered\n"
30
+ )
31
+ path = handle.name
32
+
33
+ args = Namespace(
34
+ paths=[path],
35
+ start="2026-03-24T10:01:00Z",
36
+ end="2026-03-24T10:06:00Z",
37
+ assume_timezone="UTC",
38
+ keep_undated=False,
39
+ count_only=False,
40
+ )
41
+
42
+ with patch.object(MODULE, "parse_args", return_value=args), patch(
43
+ "sys.stdout", new_callable=io.StringIO
44
+ ) as stdout:
45
+ code = MODULE.main()
46
+
47
+ self.assertEqual(code, 0)
48
+ self.assertEqual(stdout.getvalue().strip(), "2026-03-24T10:05:00Z ERROR failed")
49
+
50
+ def test_keep_undated_lines_when_requested(self) -> None:
51
+ with tempfile.NamedTemporaryFile("w+", encoding="utf-8", delete=False) as handle:
52
+ handle.write(
53
+ "2026-03-24T10:00:00Z INFO boot\n"
54
+ "plain undated line\n"
55
+ )
56
+ path = handle.name
57
+
58
+ args = Namespace(
59
+ paths=[path],
60
+ start=None,
61
+ end=None,
62
+ assume_timezone="UTC",
63
+ keep_undated=True,
64
+ count_only=False,
65
+ )
66
+
67
+ with patch.object(MODULE, "parse_args", return_value=args), patch(
68
+ "sys.stdout", new_callable=io.StringIO
69
+ ) as stdout:
70
+ code = MODULE.main()
71
+
72
+ self.assertEqual(code, 0)
73
+ self.assertIn("plain undated line", stdout.getvalue())
74
+
75
+ def test_rejects_inverted_window(self) -> None:
76
+ args = Namespace(
77
+ paths=[],
78
+ start="2026-03-24T10:10:00Z",
79
+ end="2026-03-24T10:00:00Z",
80
+ assume_timezone="UTC",
81
+ keep_undated=False,
82
+ count_only=False,
83
+ )
84
+
85
+ with patch.object(MODULE, "parse_args", return_value=args), patch(
86
+ "sys.stderr", new_callable=io.StringIO
87
+ ) as stderr:
88
+ code = MODULE.main()
89
+
90
+ self.assertEqual(code, 1)
91
+ self.assertIn("--start", stderr.getvalue())
92
+
93
+
94
+ if __name__ == "__main__":
95
+ unittest.main()
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import tempfile
8
+ import unittest
9
+ from argparse import Namespace
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+
14
+ SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "search_logs.py"
15
+ SCRIPT_DIR = SCRIPT_PATH.parent
16
+ if str(SCRIPT_DIR) not in __import__("sys").path:
17
+ __import__("sys").path.insert(0, str(SCRIPT_DIR))
18
+ SPEC = importlib.util.spec_from_file_location("search_logs", SCRIPT_PATH)
19
+ MODULE = importlib.util.module_from_spec(SPEC)
20
+ SPEC.loader.exec_module(MODULE)
21
+
22
+
23
+ class SearchLogsTests(unittest.TestCase):
24
+ def test_keyword_search_respects_time_window(self) -> None:
25
+ with tempfile.NamedTemporaryFile("w+", encoding="utf-8", delete=False) as handle:
26
+ handle.write(
27
+ "2026-03-24T10:00:00Z INFO boot\n"
28
+ "2026-03-24T10:05:00Z ERROR payment timeout\n"
29
+ "2026-03-24T10:10:00Z ERROR payment timeout\n"
30
+ )
31
+ path = handle.name
32
+
33
+ args = Namespace(
34
+ paths=[path],
35
+ keyword=["timeout"],
36
+ regex=[],
37
+ mode="any",
38
+ ignore_case=False,
39
+ start="2026-03-24T10:01:00Z",
40
+ end="2026-03-24T10:06:00Z",
41
+ assume_timezone="UTC",
42
+ before_context=0,
43
+ after_context=0,
44
+ count_only=False,
45
+ )
46
+
47
+ with patch.object(MODULE, "parse_args", return_value=args), patch(
48
+ "sys.stdout", new_callable=io.StringIO
49
+ ) as stdout:
50
+ code = MODULE.main()
51
+
52
+ self.assertEqual(code, 0)
53
+ self.assertEqual(
54
+ stdout.getvalue().strip(),
55
+ "2026-03-24T10:05:00Z ERROR payment timeout",
56
+ )
57
+
58
+ def test_all_mode_requires_every_matcher(self) -> None:
59
+ matchers = [
60
+ lambda line: "timeout" in line,
61
+ lambda line: "payment" in line,
62
+ ]
63
+
64
+ self.assertTrue(MODULE.line_matches("payment timeout", matchers, "all"))
65
+ self.assertFalse(MODULE.line_matches("timeout only", matchers, "all"))
66
+
67
+ def test_count_only_reports_match_total(self) -> None:
68
+ with tempfile.NamedTemporaryFile("w+", encoding="utf-8", delete=False) as handle:
69
+ handle.write(
70
+ "2026-03-24T10:00:00Z INFO boot\n"
71
+ "2026-03-24T10:05:00Z ERROR payment timeout\n"
72
+ "2026-03-24T10:06:00Z WARN retry timeout\n"
73
+ )
74
+ path = handle.name
75
+
76
+ args = Namespace(
77
+ paths=[path],
78
+ keyword=["timeout"],
79
+ regex=[],
80
+ mode="any",
81
+ ignore_case=True,
82
+ start=None,
83
+ end=None,
84
+ assume_timezone="UTC",
85
+ before_context=0,
86
+ after_context=0,
87
+ count_only=True,
88
+ )
89
+
90
+ with patch.object(MODULE, "parse_args", return_value=args), patch(
91
+ "sys.stdout", new_callable=io.StringIO
92
+ ) as stdout:
93
+ code = MODULE.main()
94
+
95
+ self.assertEqual(code, 0)
96
+ self.assertEqual(stdout.getvalue().strip(), "2")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ unittest.main()