diff --git a/AGENTS.md b/AGENTS.md index 8c00bf1..9d77104 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ You are an expert Software Engineer working on this project. Your primary respon **"If it's not documented in `docs/tasks/`, it didn't happen."** ## Workflow -1. **Pick a Task**: Run `python3 scripts/tasks.py context` to see active tasks, or `list` to see pending ones. +1. **Pick a Task**: Run `python3 scripts/tasks.py next` to find the best task, `context` to see active tasks, or `list` to see pending ones. 2. **Plan & Document**: * **Memory Check**: Run `python3 scripts/memory.py list` (or use the Memory Skill) to recall relevant long-term information. * **Security Check**: Ask the user about specific security considerations for this task. diff --git a/AGENTS.md.bak b/AGENTS.md.bak new file mode 100644 index 0000000..8c00bf1 --- /dev/null +++ b/AGENTS.md.bak @@ -0,0 +1,117 @@ +# AI Agent Instructions + +You are an expert Software Engineer working on this project. Your primary responsibility is to implement features and fixes while strictly adhering to the **Task Documentation System**. + +## Core Philosophy +**"If it's not documented in `docs/tasks/`, it didn't happen."** + +## Workflow +1. **Pick a Task**: Run `python3 scripts/tasks.py context` to see active tasks, or `list` to see pending ones. +2. **Plan & Document**: + * **Memory Check**: Run `python3 scripts/memory.py list` (or use the Memory Skill) to recall relevant long-term information. + * **Security Check**: Ask the user about specific security considerations for this task. + * If starting a new task, use `scripts/tasks.py create` (or `python3 scripts/tasks.py create`) to generate a new task file. + * Update the task status: `python3 scripts/tasks.py update [TASK_ID] in_progress`. +3. **Implement**: Write code, run tests. +4. **Update Documentation Loop**: + * As you complete sub-tasks, check them off in the task document. + * If you hit a blocker, update status to `wip_blocked` and describe the issue in the file. + * Record key architectural decisions in the task document. + * **Memory Update**: If you learn something valuable for the long term, use `scripts/memory.py create` to record it. +5. **Review & Verify**: + * Once implementation is complete, update status to `review_requested`: `python3 scripts/tasks.py update [TASK_ID] review_requested`. + * Ask a human or another agent to review the code. + * Once approved and tested, update status to `verified`. +6. **Finalize**: + * Update status to `completed`: `python3 scripts/tasks.py update [TASK_ID] completed`. + * Record actual effort in the file. + * Ensure all acceptance criteria are met. + +## Tools +* **Wrapper**: `./scripts/tasks` (Checks for Python, recommended). +* **Next**: `./scripts/tasks next` (Finds the best task to work on). +* **Create**: `./scripts/tasks create [category] "Title"` +* **List**: `./scripts/tasks list [--status pending]` +* **Context**: `./scripts/tasks context` +* **Update**: `./scripts/tasks update [ID] [status]` +* **Migrate**: `./scripts/tasks migrate` (Migrate legacy tasks to new format) +* **Link**: `./scripts/tasks link [ID] [DEP_ID]` (Add dependency). +* **Unlink**: `./scripts/tasks unlink [ID] [DEP_ID]` (Remove dependency). +* **Index**: `./scripts/tasks index` (Generate INDEX.yaml). +* **Graph**: `./scripts/tasks graph` (Visualize dependencies). +* **Validate**: `./scripts/tasks validate` (Check task files). +* **Memory**: `./scripts/memory.py [create|list|read]` +* **JSON Output**: Add `--format json` to any command for machine parsing. + +## Documentation Reference +* **Guide**: Read `docs/tasks/GUIDE.md` for strict formatting and process rules. +* **Architecture**: Refer to `docs/architecture/` for system design. +* **Features**: Refer to `docs/features/` for feature specifications. +* **Security**: Refer to `docs/security/` for risk assessments and mitigations. +* **Memories**: Refer to `docs/memories/` for long-term project context. + +## Code Style & Standards +* Follow the existing patterns in the codebase. +* Ensure all new code is covered by tests (if testing infrastructure exists). + +## PR Review Methodology +When performing a PR review, follow this "Human-in-the-loop" process to ensure depth and efficiency. + +### 1. Preparation +1. **Create Task**: `python3 scripts/tasks.py create review "Review PR #: "` +2. **Fetch Details**: Use `gh` to get the PR context. + * `gh pr view <N>` + * `gh pr diff <N>` + +### 2. Analysis & Planning (The "Review Plan") +**Do not review line-by-line yet.** Instead, analyze the changes and document a **Review Plan** in the task file (or present it for approval). + +Your plan must include: +* **High-Level Summary**: Purpose, new APIs, breaking changes. +* **Dependency Check**: New libraries, maintenance status, security. +* **Impact Assessment**: Effect on existing code/docs. +* **Focus Areas**: Prioritized list of files/modules to check. +* **Suggested Comments**: Draft comments for specific lines. + * Format: `File: <path> | Line: <N> | Comment: <suggestion>` + * Tone: Friendly, suggestion-based ("Consider...", "Nit: ..."). + +### 3. Execution +Once the human approves the plan and comments: +1. **Pending Review**: Create a pending review using `gh`. + * `COMMIT_SHA=$(gh pr view <N> --json headRefOid -q .headRefOid)` + * `gh api repos/{owner}/{repo}/pulls/{N}/reviews -f commit_id="$COMMIT_SHA"` +2. **Batch Comments**: Add comments to the pending review. + * `gh api repos/{owner}/{repo}/pulls/{N}/comments -f body="..." -f path="..." -f commit_id="$COMMIT_SHA" -F line=<L> -f side="RIGHT"` +3. **Submit**: + * `gh pr review <N> --approve --body "Summary..."` (or `--request-changes`). + +### 4. Close Task +* Update task status to `completed`. + +## Project Specific Instructions + +### Core Directives +- **API First**: The Bible AI API is the primary source for data. Scraping (`pkg/app/passage.go` fallback) is deprecated and should be avoided for new features. +- **Secrets**: Do not commit secrets. Use `pkg/secrets` to retrieve them from Environment or Google Secret Manager. +- **Testing**: Run tests from the root using `go test ./pkg/...`. + +### Code Guidelines +- **Go Version**: 1.24+ +- **Naming**: + - Variables: `camelCase` + - Functions: `PascalCase` (exported), `camelCase` (internal) + - Packages: `underscore_case` +- **Structure**: + - `pkg/app`: Business logic. + - `pkg/bot`: Platform integration. + - `pkg/utils`: Shared utilities. + +### Local Development +- **Setup**: Create a `.env` file with `TELEGRAM_ID` and `TELEGRAM_ADMIN_ID`. +- **Run**: `go run main.go` +- **Testing**: Use `ngrok` to tunnel webhooks or send mock HTTP requests. + +## Agent Interoperability +- **Task Manager Skill**: `.claude/skills/task_manager/` +- **Memory Skill**: `.claude/skills/memory/` +- **Tool Definitions**: `docs/interop/tool_definitions.json` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d1fd6d6..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,112 +0,0 @@ -# AI Agent Instructions - -You are an expert Software Engineer working on this project. Your primary responsibility is to implement features and fixes while strictly adhering to the **Task Documentation System**. - -## Core Philosophy -**"If it's not documented in `docs/tasks/`, it didn't happen."** - -## Workflow -1. **Pick a Task**: Run `python3 scripts/tasks.py next` to find the best task, `context` to see active tasks, or `list` to see pending ones. -2. **Plan & Document**: - * **Memory Check**: Run `python3 scripts/memory.py list` (or use the Memory Skill) to recall relevant long-term information. - * **Security Check**: Ask the user about specific security considerations for this task. - * If starting a new task, use `scripts/tasks.py create` (or `python3 scripts/tasks.py create`) to generate a new task file. - * Update the task status: `python3 scripts/tasks.py update [TASK_ID] in_progress`. -3. **Implement**: Write code, run tests. -4. **Update Documentation Loop**: - * As you complete sub-tasks, check them off in the task document. - * If you hit a blocker, update status to `wip_blocked` and describe the issue in the file. - * Record key architectural decisions in the task document. - * **Memory Update**: If you learn something valuable for the long term, use `scripts/memory.py create` to record it. -5. **Review & Verify**: - * Once implementation is complete, update status to `review_requested`: `python3 scripts/tasks.py update [TASK_ID] review_requested`. - * Ask a human or another agent to review the code. - * Once approved and tested, update status to `verified`. -6. **Finalize**: - * Update status to `completed`: `python3 scripts/tasks.py update [TASK_ID] completed`. - * Record actual effort in the file. - * Ensure all acceptance criteria are met. - -## Tools -* **Wrapper**: `./scripts/tasks` (Checks for Python, recommended). -* **Next**: `./scripts/tasks next` (Finds the best task to work on). -* **Create**: `./scripts/tasks create [category] "Title"` -* **List**: `./scripts/tasks list [--status pending]` -* **Context**: `./scripts/tasks context` -* **Update**: `./scripts/tasks update [ID] [status]` -* **Migrate**: `./scripts/tasks migrate` (Migrate legacy tasks to new format) -* **Memory**: `./scripts/memory.py [create|list|read]` -* **JSON Output**: Add `--format json` to any command for machine parsing. - -## Documentation Reference -* **Guide**: Read `docs/tasks/GUIDE.md` for strict formatting and process rules. -* **Architecture**: Refer to `docs/architecture/` for system design. -* **Features**: Refer to `docs/features/` for feature specifications. -* **Security**: Refer to `docs/security/` for risk assessments and mitigations. -* **Memories**: Refer to `docs/memories/` for long-term project context. - -## Code Style & Standards -* Follow the existing patterns in the codebase. -* Ensure all new code is covered by tests (if testing infrastructure exists). - -## PR Review Methodology -When performing a PR review, follow this "Human-in-the-loop" process to ensure depth and efficiency. - -### 1. Preparation -1. **Create Task**: `python3 scripts/tasks.py create review "Review PR #<N>: <Title>"` -2. **Fetch Details**: Use `gh` to get the PR context. - * `gh pr view <N>` - * `gh pr diff <N>` - -### 2. Analysis & Planning (The "Review Plan") -**Do not review line-by-line yet.** Instead, analyze the changes and document a **Review Plan** in the task file (or present it for approval). - -Your plan must include: -* **High-Level Summary**: Purpose, new APIs, breaking changes. -* **Dependency Check**: New libraries, maintenance status, security. -* **Impact Assessment**: Effect on existing code/docs. -* **Focus Areas**: Prioritized list of files/modules to check. -* **Suggested Comments**: Draft comments for specific lines. - * Format: `File: <path> | Line: <N> | Comment: <suggestion>` - * Tone: Friendly, suggestion-based ("Consider...", "Nit: ..."). - -### 3. Execution -Once the human approves the plan and comments: -1. **Pending Review**: Create a pending review using `gh`. - * `COMMIT_SHA=$(gh pr view <N> --json headRefOid -q .headRefOid)` - * `gh api repos/{owner}/{repo}/pulls/{N}/reviews -f commit_id="$COMMIT_SHA"` -2. **Batch Comments**: Add comments to the pending review. - * `gh api repos/{owner}/{repo}/pulls/{N}/comments -f body="..." -f path="..." -f commit_id="$COMMIT_SHA" -F line=<L> -f side="RIGHT"` -3. **Submit**: - * `gh pr review <N> --approve --body "Summary..."` (or `--request-changes`). - -### 4. Close Task -* Update task status to `completed`. - -## Project Specific Instructions - -### Core Directives -- **API First**: The Bible AI API is the primary source for data. Scraping (`pkg/app/passage.go` fallback) is deprecated and should be avoided for new features. -- **Secrets**: Do not commit secrets. Use `pkg/secrets` to retrieve them from Environment or Google Secret Manager. -- **Testing**: Run tests from the root using `go test ./pkg/...`. - -### Code Guidelines -- **Go Version**: 1.24+ -- **Naming**: - - Variables: `camelCase` - - Functions: `PascalCase` (exported), `camelCase` (internal) - - Packages: `underscore_case` -- **Structure**: - - `pkg/app`: Business logic. - - `pkg/bot`: Platform integration. - - `pkg/utils`: Shared utilities. - -### Local Development -- **Setup**: Create a `.env` file with `TELEGRAM_ID` and `TELEGRAM_ADMIN_ID`. -- **Run**: `go run main.go` -- **Testing**: Use `ngrok` to tunnel webhooks or send mock HTTP requests. - -## Agent Interoperability -- **Task Manager Skill**: `.claude/skills/task_manager/` -- **Memory Skill**: `.claude/skills/memory/` -- **Tool Definitions**: `docs/interop/tool_definitions.json` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..b9b204e --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,48 @@ +# Testing Strategy + +This project employs a hybrid testing strategy to ensure code quality while minimizing external dependencies and costs. + +## Test Categories + +### 1. Unit Tests (Standard) +* **Default Behavior:** By default, all tests run in "mock mode". +* **Goal:** Fast, reliable, and cost-free verification of logic. +* **Mechanism:** External services (Bible AI API, BibleGateway scraping) are mocked using function replacement (e.g., `SubmitQuery`, `GetPassageHTML`) or interface mocking. +* **Execution:** these tests are run automatically on every Pull Request (MR). + +### 2. Integration Tests (Live) +* **Conditional Behavior:** Specific tests are capable of switching to "live mode" when appropriate environment variables are detected. +* **Goal:** Verify that the application correctly interacts with real external services (Contract Testing) and that credentials/configurations are valid. +* **Execution:** These tests should be run on a scheduled basis (e.g., nightly or weekly) or manually when verifying infrastructure changes. + +## Live Tests & Configuration + +The following tests support live execution: + +### `TestSubmitQuery` +* **File:** `pkg/app/api_client_test.go` +* **Description:** Verifies connectivity to the Bible AI API. +* **Trigger:** + * `BIBLE_API_URL` is set AND + * `BIBLE_API_URL` is NOT `https://example.com` +* **Required Variables:** + * `BIBLE_API_URL`: The endpoint of the Bible AI API. + * `BIBLE_API_KEY`: A valid API key. +* **Rationale:** Ensures that the client code (request marshaling, auth headers) matches the actual API expectation and that the API is reachable. + +### `TestUserDatabaseIntegration` +* **File:** `pkg/app/database_integration_test.go` +* **Description:** Verifies Read/Write operations to Google Cloud Firestore/Datastore. +* **Trigger:** + * `GCLOUD_PROJECT_ID` is set. +* **Required Variables:** + * `GCLOUD_PROJECT_ID`: The Google Cloud Project ID. + * *Note:* Requires active Google Cloud credentials (e.g., `GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth`). +* **Rationale:** Verifies that database permissions and client initialization are correct, preventing runtime errors in production. Uses a specific test user ID (`test-integration-user-DO-NOT-DELETE`) to avoid affecting real user data. + +## Rationale for Strategy + +1. **Cost Reduction:** The Bible AI API may incur costs per call. Mocking prevents racking up bills during routine development. +2. **Speed:** Live calls are slow. Mocked tests run instantly. +3. **Reliability:** External services can be flaky. Mocked tests only fail if the code is broken. +4. **Verification:** We still need to know if the API changed or if our secrets are wrong. The conditional integration tests provide this safety net without the daily cost/latency penalty. diff --git a/docs/tasks/GUIDE.md b/docs/tasks/GUIDE.md index 7fd2292..77b8b86 100644 --- a/docs/tasks/GUIDE.md +++ b/docs/tasks/GUIDE.md @@ -91,21 +91,17 @@ Use the `scripts/tasks` wrapper to manage tasks. ./scripts/tasks update [TASK_ID] verified ./scripts/tasks update [TASK_ID] completed -# Migrate legacy tasks (if updating from older version) -./scripts/tasks migrate - # Manage Dependencies -./scripts/tasks link [TASK_ID] [DEPENDENCY_ID] -./scripts/tasks unlink [TASK_ID] [DEPENDENCY_ID] +./scripts/tasks link [TASK_ID] [DEP_ID] +./scripts/tasks unlink [TASK_ID] [DEP_ID] -# Generate Dependency Index (docs/tasks/INDEX.yaml) -./scripts/tasks index +# Visualization & Analysis +./scripts/tasks graph # Show dependency graph +./scripts/tasks index # Generate INDEX.yaml +./scripts/tasks validate # Check for errors -# Visualize Dependencies (Mermaid Graph) -./scripts/tasks graph - -# Validate Task Files -./scripts/tasks validate +# Migrate legacy tasks (if updating from older version) +./scripts/tasks migrate ``` ## Agile Methodology diff --git a/docs/tasks/migration/MIGRATION-20251229-060122-RTG-update-agent-harness.md b/docs/tasks/migration/MIGRATION-20251229-060122-RTG-update-agent-harness.md new file mode 100644 index 0000000..04111e8 --- /dev/null +++ b/docs/tasks/migration/MIGRATION-20251229-060122-RTG-update-agent-harness.md @@ -0,0 +1,14 @@ +--- +id: MIGRATION-20251229-060122-RTG +status: completed +title: Update Agent Harness +priority: medium +created: 2025-12-29 06:01:22 +category: migration +dependencies: +type: task +--- + +# Update Agent Harness + +To be determined diff --git a/pkg/app/api_client_test.go b/pkg/app/api_client_test.go index e2fac62..88fb4ef 100644 --- a/pkg/app/api_client_test.go +++ b/pkg/app/api_client_test.go @@ -1,15 +1,26 @@ package app import ( + "os" "testing" ) func TestSubmitQuery(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Force cleanup of environment to ensure we test Secret Manager fallback - // This handles cases where the runner might have lingering env vars - defer SetEnv("BIBLE_API_URL", "https://example.com")() - defer SetEnv("BIBLE_API_KEY", "api_key")() + // Check if we should run integration test against real API + // If BIBLE_API_URL is set and not example.com, we assume integration test mode + realURL, hasURL := os.LookupEnv("BIBLE_API_URL") + if hasURL && realURL != "" && realURL != "https://example.com" { + t.Logf("Running integration test against real API: %s", realURL) + // Ensure we have a key + if _, hasKey := os.LookupEnv("BIBLE_API_KEY"); !hasKey { + t.Log("Warning: BIBLE_API_URL set but BIBLE_API_KEY missing. Test might fail.") + } + } else { + // Mock mode + defer SetEnv("BIBLE_API_URL", "https://example.com")() + defer SetEnv("BIBLE_API_KEY", "api_key")() + } ResetAPIConfigCache() diff --git a/pkg/app/devo_test.go b/pkg/app/devo_test.go index 0ad7fcb..e10fbad 100644 --- a/pkg/app/devo_test.go +++ b/pkg/app/devo_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "golang.org/x/net/html" + "github.com/julwrites/BotPlatform/pkg/def" "github.com/julwrites/ScriptureBot/pkg/utils" ) @@ -81,6 +83,19 @@ func TestGetDevotionalData(t *testing.T) { defer UnsetEnv("BIBLE_API_KEY")() ResetAPIConfigCache() + // Mock GetPassageHTML to prevent external calls during fallback + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` + <div class="bcv">Genesis 1</div> + <div class="passage-text"> + <p>Mock devotional content.</p> + </div> + `) + } + var env def.SessionData env.Props = map[string]interface{}{"ResourcePath": "../../resource"} env.Res = GetDevotionalData(env, "DTMSV") diff --git a/pkg/app/passage.go b/pkg/app/passage.go index 5a5a7d3..2c610bc 100644 --- a/pkg/app/passage.go +++ b/pkg/app/passage.go @@ -8,6 +8,7 @@ import ( "log" "net/url" "strings" + stdhtml "html" "golang.org/x/net/html" @@ -59,20 +60,30 @@ func isNextSiblingBr(node *html.Node) bool { } func ParseNodesForPassage(node *html.Node) string { - var text string var parts []string for child := node.FirstChild; child != nil; child = child.NextSibling { - parts = append(parts, text) + // Filter out footnotes sections/cross-refs if they appear as divs + if child.Type == html.ElementNode { + for _, attr := range child.Attr { + if attr.Key == "class" { + if strings.Contains(attr.Val, "footnotes") || strings.Contains(attr.Val, "cross-refs") { + continue + } + } + } + } switch tag := child.Data; tag { case "span": + // Keep existing logic for span (likely poetry lines in legacy/scraped HTML) childText := ParseNodesForPassage(child) parts = append(parts, childText) if len(strings.TrimSpace(childText)) > 0 && !isNextSiblingBr(child) { parts = append(parts, "\n") } case "sup": + // Handle superscripts (verse numbers/footnotes) isFootnote := func(node *html.Node) bool { for _, attr := range node.Attr { if attr.Key == "class" && attr.Val == "footnote" { @@ -85,67 +96,62 @@ func ParseNodesForPassage(node *html.Node) string { break } childText := ParseNodesForPassage(child) + // Use TelegramSuperscript for unicode conversion if len(childText) > 0 { - parts = append(parts, fmt.Sprintf("^%s^", childText)) + parts = append(parts, platform.TelegramSuperscript(childText)) } break case "p": parts = append(parts, ParseNodesForPassage(child)) - break - case "b": - parts = append(parts, platform.TelegramBold(ParseNodesForPassage(child))) - case "i": - parts = append(parts, platform.TelegramItalics(ParseNodesForPassage(child))) - break + parts = append(parts, "\n\n") + case "b", "strong": + parts = append(parts, fmt.Sprintf("<b>%s</b>", ParseNodesForPassage(child))) + case "i", "em": + parts = append(parts, fmt.Sprintf("<i>%s</i>", ParseNodesForPassage(child))) + case "h1", "h2", "h3", "h4", "h5", "h6": + // Ignore "Footnotes" or "Cross references" headers + headerText := ParseNodesForPassage(child) + if headerText == "Footnotes" || headerText == "Cross references" { + continue + } + parts = append(parts, fmt.Sprintf("\n\n<b>%s</b>\n", headerText)) + case "ul", "ol": + parts = append(parts, ParseNodesForPassage(child)) + case "li": + parts = append(parts, fmt.Sprintf("• %s\n", ParseNodesForPassage(child))) case "br": parts = append(parts, "\n") - break + case "div": + parts = append(parts, ParseNodesForPassage(child)) default: - parts = append(parts, child.Data) + if child.Type == html.TextNode { + parts = append(parts, stdhtml.EscapeString(child.Data)) + } else if child.Type == html.ElementNode { + // Recurse for unknown elements to preserve content + parts = append(parts, ParseNodesForPassage(child)) + } } } - text = strings.Join(parts, "") - - if node.Data == "h1" || node.Data == "h2" || node.Data == "h3" || node.Data == "h4" { - text = fmt.Sprintf("*%s*", text) - } - return text + return strings.Join(parts, "") } func GetPassage(ref string, doc *html.Node, version string) string { - filtNodes := utils.FilterTree(doc, func(child *html.Node) bool { - switch tag := child.Data; tag { - case "h1": - fallthrough - case "h2": - fallthrough - case "h3": - fallthrough - case "h4": - if child.FirstChild.Data == "Footnotes" || child.FirstChild.Data == "Cross references" { - return false - } - fallthrough - case "p": - return true - } - return false - }) + // Replaced FilterTree with direct parsing of the root node + // This allows handling arbitrary structure (divs, lists) returned by the API - textBlocks := utils.MapNodeListToString(filtNodes, ParseNodesForPassage) + text := ParseNodesForPassage(doc) var passage strings.Builder if len(ref) > 0 { - refString := fmt.Sprintf("_%s_ (%s)", ref, version) + // Use HTML formatting for reference + refString := fmt.Sprintf("<i>%s</i> (%s)", ref, version) passage.WriteString(refString) } - for _, block := range textBlocks { - passage.WriteString("\n") - passage.WriteString(block) - } + passage.WriteString("\n") + passage.WriteString(strings.TrimSpace(text)) return passage.String() } @@ -158,6 +164,11 @@ func ParsePassageFromHtml(ref string, rawHtml string, version string) string { return rawHtml } + // html.Parse returns a doc with html->body structure. + // GetPassage -> ParseNodesForPassage will traverse it. + // We might want to find 'body' to avoid processing 'head'? + // ParseNodesForPassage iterates children. doc->html->body. + // We can let it recurse. return strings.TrimSpace(GetPassage(ref, doc, version)) } @@ -181,6 +192,7 @@ func GetBiblePassageFallback(env def.SessionData) def.SessionData { // Attempt to get the passage env.Res.Message = GetPassage(ref, passageNode, config.Version) + env.Res.ParseMode = def.TELEGRAM_PARSE_MODE_HTML return env } @@ -224,6 +236,7 @@ func GetBiblePassage(env def.SessionData) def.SessionData { if len(resp.Verse) > 0 { env.Res.Message = ParsePassageFromHtml(env.Msg.Message, resp.Verse, config.Version) + env.Res.ParseMode = def.TELEGRAM_PARSE_MODE_HTML return env } } diff --git a/pkg/app/passage_test.go b/pkg/app/passage_test.go index 21f7c21..29fdaeb 100644 --- a/pkg/app/passage_test.go +++ b/pkg/app/passage_test.go @@ -1,16 +1,33 @@ package app import ( + "errors" "strings" "testing" + "golang.org/x/net/html" + "github.com/julwrites/BotPlatform/pkg/def" "github.com/julwrites/ScriptureBot/pkg/utils" ) +func mockGetPassageHTML(htmlStr string) *html.Node { + doc, _ := html.Parse(strings.NewReader(htmlStr)) + return doc +} + func TestGetReference(t *testing.T) { - doc := GetPassageHTML("gen 1", "NIV") + // Mock GetPassageHTML + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` + <div class="bcv">Genesis 1</div> + `) + } + doc := GetPassageHTML("gen 1", "NIV") ref := GetReference(doc) if ref != "Genesis 1" { @@ -19,6 +36,18 @@ func TestGetReference(t *testing.T) { } func TestGetPassage(t *testing.T) { + // Mock GetPassageHTML + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` + <div class="passage-text"> + <p>In the beginning was the Word.</p> + </div> + `) + } + doc := GetPassageHTML("john 8", "NIV") passage := GetPassage("John 8", doc, "NIV") @@ -83,6 +112,10 @@ func TestGetBiblePassage(t *testing.T) { if len(env.Res.Message) < 10 { t.Errorf("Expected passage text, got '%s'", env.Res.Message) } + // Verify ParseMode is set + if env.Res.ParseMode != "HTML" { + t.Errorf("Expected ParseMode 'HTML', got '%s'", env.Res.ParseMode) + } }) t.Run("Empty", func(t *testing.T) { @@ -100,20 +133,63 @@ func TestGetBiblePassage(t *testing.T) { t.Errorf("Expected failure message, got '%s'", env.Res.Message) } }) + + t.Run("Fallback: Scrape", func(t *testing.T) { + defer UnsetEnv("BIBLE_API_URL")() + defer UnsetEnv("BIBLE_API_KEY")() + ResetAPIConfigCache() + + // Mock GetPassageHTML for fallback + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` + <div class="bcv">Genesis 1</div> + <div class="passage-text"> + <p>In the beginning God created the heavens and the earth.</p> + </div> + `) + } + + var env def.SessionData + env.Msg.Message = "gen 1" + var conf utils.UserConfig + conf.Version = "NIV" + env = utils.SetUserConfig(env, utils.SerializeUserConfig(conf)) + + // Override SubmitQuery to force failure + originalSubmitQuerySub := SubmitQuery + defer func() { SubmitQuery = originalSubmitQuerySub }() + SubmitQuery = func(req QueryRequest, result interface{}) error { + return errors.New("forced api error") + } + + env = GetBiblePassage(env) + + if !strings.Contains(env.Res.Message, "In the beginning") { + t.Errorf("Expected fallback passage content, got '%s'", env.Res.Message) + } + // Fallback should also use HTML mode + if env.Res.ParseMode != "HTML" { + t.Errorf("Expected ParseMode 'HTML' in fallback, got '%s'", env.Res.ParseMode) + } + }) } func TestParsePassageFromHtml(t *testing.T) { t.Run("Valid HTML with superscript", func(t *testing.T) { html := `<p><span><sup>12 </sup>But to all who did receive him, who believed in his name, he gave the right to become children of God,</span></p>` - expected := `^12 ^But to all who did receive him, who believed in his name, he gave the right to become children of God,` + // Updated expectation: unicode superscripts and HTML formatting + expected := `¹²But to all who did receive him, who believed in his name, he gave the right to become children of God,` if got := ParsePassageFromHtml("", html, ""); got != expected { - t.Errorf("ParsePassageFromHtml() = %v, want %v", got, expected) + t.Errorf("ParsePassageFromHtml() = %s, want %s", got, expected) } }) t.Run("HTML with italics", func(t *testing.T) { html := `<p><i>This is italic.</i></p>` - expected := `_This is italic._` + expected := `<i>This is italic.</i>` if got := ParsePassageFromHtml("", html, ""); got != expected { t.Errorf("ParsePassageFromHtml() = %v, want %v", got, expected) } @@ -121,7 +197,7 @@ func TestParsePassageFromHtml(t *testing.T) { t.Run("HTML with bold", func(t *testing.T) { html := `<p><b>This is bold.</b></p>` - expected := `*This is bold.*` + expected := `<b>This is bold.</b>` if got := ParsePassageFromHtml("", html, ""); got != expected { t.Errorf("ParsePassageFromHtml() = %v, want %v", got, expected) } @@ -161,21 +237,38 @@ func TestParsePassageFromHtml(t *testing.T) { t.Run("Nested HTML tags", func(t *testing.T) { html := `<p><b>This is bold, <i>and this is italic.</i></b></p>` - expected := `*This is bold, _and this is italic._*` + expected := `<b>This is bold, <i>and this is italic.</i></b>` if got := ParsePassageFromHtml("", html, ""); got != expected { t.Errorf("ParsePassageFromHtml() = %v, want %v", got, expected) } }) - t.Run("MarkdownV2 escaping", func(t *testing.T) { - // Note: We no longer escape explicitly in ParsePassageFromHtml as we rely on the platform - // to handle it later (via PostTelegram). - // However, returning raw characters like * might cause issues if not handled by platform. - // For now, we expect them to be returned raw. - html := `<p>This has special characters: *_. [hello](world)!</p>` - expected := `This has special characters: *_. [hello](world)!` + t.Run("Lists", func(t *testing.T) { + html := `<ul><li>Item 1</li><li>Item 2</li></ul>` + // Note: The ParseNodesForPassage appends newline after each Item. + // strings.TrimSpace removes the last newline. + // Item 1\nItem 2\n -> Item 1\nItem 2 + expected := "• Item 1\n• Item 2" if got := ParsePassageFromHtml("", html, ""); got != expected { - t.Errorf("ParsePassageFromHtml() = %v, want %v", got, expected) + t.Errorf("ParsePassageFromHtml() = %q, want %q", got, expected) + } + }) + + t.Run("Headers", func(t *testing.T) { + html := `<h1>Header</h1>` + // Code: \n\n<b>Header</b>\n + // TrimSpace -> <b>Header</b> + expected := "<b>Header</b>" + if got := ParsePassageFromHtml("", html, ""); got != expected { + t.Errorf("ParsePassageFromHtml() = %q, want %q", got, expected) + } + }) + + t.Run("Divs and escaping", func(t *testing.T) { + html := `<div>Text <with> symbols</div>` + expected := "Text <with> symbols" + if got := ParsePassageFromHtml("", html, ""); got != expected { + t.Errorf("ParsePassageFromHtml() = %q, want %q", got, expected) } }) } diff --git a/templates/GUIDE.md b/templates/GUIDE.md index 3d0a944..77b8b86 100644 --- a/templates/GUIDE.md +++ b/templates/GUIDE.md @@ -91,6 +91,15 @@ Use the `scripts/tasks` wrapper to manage tasks. ./scripts/tasks update [TASK_ID] verified ./scripts/tasks update [TASK_ID] completed +# Manage Dependencies +./scripts/tasks link [TASK_ID] [DEP_ID] +./scripts/tasks unlink [TASK_ID] [DEP_ID] + +# Visualization & Analysis +./scripts/tasks graph # Show dependency graph +./scripts/tasks index # Generate INDEX.yaml +./scripts/tasks validate # Check for errors + # Migrate legacy tasks (if updating from older version) ./scripts/tasks migrate ``` diff --git a/templates/maintenance_mode.md b/templates/maintenance_mode.md index 963e0b7..3d53c80 100644 --- a/templates/maintenance_mode.md +++ b/templates/maintenance_mode.md @@ -29,7 +29,6 @@ You are an expert Software Engineer working on this project. Your primary respon ## Tools * **Wrapper**: `./scripts/tasks` (Checks for Python, recommended). -* **Next**: `./scripts/tasks next` (Finds the best task to work on). * **Create**: `./scripts/tasks create [category] "Title"` * **List**: `./scripts/tasks list [--status pending]` * **Context**: `./scripts/tasks context` diff --git a/templates/task.md b/templates/task.md new file mode 100644 index 0000000..58a9703 --- /dev/null +++ b/templates/task.md @@ -0,0 +1,23 @@ +# Task: {title} + +## Task Information +- **Task ID**: {task_id} +- **Status**: pending +- **Priority**: medium +- **Phase**: 1 +- **Estimated Effort**: 1 day +- **Dependencies**: None + +## Task Details + +### Description +{description} + +### Acceptance Criteria +- [ ] Criterion 1 +- [ ] Criterion 2 + +--- + +*Created: {date}* +*Status: pending*