# Store References Spec (3.1)

## Outcome

A project repo can declare, once, which stores its work draws on — and
from then on, every agent session in that repo sees an **index** of those
stores' specs inside the instructions it already reads: what exists, one
line about each, and the exact command to fetch any of them. Upstream
truth stays in the store; downstream work stays in the repo's own root;
the connection is a declaration plus citations, never redirection,
copy-paste, or per-change links.

This is the headline PM/architect-to-dev layering flow: requirements
live in `team-context`, the dev's agent writing a low-level design in
the app repo discovers them from config, fetches what it needs with
`--store`, and cites them.

## Locked Decisions (roadmap, 2026-06-11)

1. **Index, not inline.** Referenced-store content is never inlined into
   generated instructions. Instructions carry an index (spec ids,
   one-line summaries, the fetch recipe via `--store`) built **live from
   the registered checkout at assembly time**; the agent fetches what it
   needs. Inlining would freeze upstream content at generation time —
   the copy-paste failure this effort exists to kill.
2. **Declarations live in `openspec/config.yaml`.** A `references:` list
   of store ids, sharing the one id namespace (kebab grammar) locked for
   Phase 3.
3. **Relationships are location, declaration, or citation — never
   managed artifact links.** No per-change edge objects; artifact-level
   derivation ("derives from team-context/billing") is prose citation.
4. **Root resolution is untouched.** References are read-only context. A
   declared reference never changes where commands act; writing to a
   referenced store remains an explicit `--store` action and a separate
   change in that store. The fixed precedence (explicit `--store` →
   nearest local root → declared fallback (3.2) → error) gains nothing
   from this slice.
5. **An unresolvable reference is reported with a clear next step, not
   silently ignored.**

## Decisions This Spec Makes (autonomous, recorded in the changelog)

1. **The index lives in both instruction surfaces, both modes.**
   Artifact instructions (`openspec instructions <artifact> --change
   ...`) and apply instructions (`openspec instructions apply`) both
   carry it, built by one shared assembler. Artifact human mode prints
   the `<referenced_stores>` XML block (mirroring `<project_context>`);
   apply human mode prints a `### Referenced Stores` markdown section
   matching its existing markdown style (`printApplyInstructionsText`
   is a real human surface — `instructions.ts:429-484`). No other
   command changes.
2. **The summary is the first non-empty line of the spec's Purpose
   section, extracted tolerantly.** NOT via `parseSpec()` — that
   throws on a missing Purpose or Requirements section
   (`src/core/parsers/markdown-parser.ts:80-86`) and the index must
   never fail on an imperfect upstream spec. The assembler scans
   sections directly; a spec with no Purpose, an unreadable file, or
   an unparseable file indexes with an empty summary (rendered as the
   bare `- <id>` line, no dangling colon). No new authoring
   requirement on stores.
3. **Problems degrade to warnings, never to silence or failure.**
   Instructions still generate; a problem entry carries the established
   `severity`/`code`/`message`/`fix` diagnostic shape (severity
   `warning` for all reference codes — JSON consumers must be able to
   distinguish degraded context from errors). New codes:
   - `reference_unresolved` — the id has no registry entry; fix names
     the id concretely: "get a checkout from a teammate and run:
     openspec store register <path> --id <the-referenced-id>" (naming
     a clone source is 3.3's job).
   - `reference_invalid_id` — entry fails the kebab id grammar; fix:
     use kebab-case ids in `references:`. (Deliberately distinct from
     the CLI's hard-error `invalid_store_id`: same grammar, different
     contract — the index degrades where the CLI refuses.)
   - `reference_root_unhealthy` — the registry resolved the id but
     anything after that failed (missing checkout path, missing or
     mismatched store metadata, unhealthy OpenSpec root per
     `inspectOpenSpecRoot().healthy === false`); fix:
     `openspec store doctor <id>`.
   A self-reference (the resolved root IS the referenced store, by
   canonicalized-path equality or matching resolved `store_id`) is
   omitted with no diagnostic — referencing yourself is meaningless; a
   root whose only reference is itself simply gets an empty index.
4. **The declaration is symmetric, and the index is exactly one level
   deep.** The assembler reads the *resolved root's* config — a store's
   own config may carry `references:`, and a session running
   `--store team-context` sees that store's upstream references. But a
   referenced store's own `references:` are never followed: no
   recursion, so circular declarations (A↔B) are structurally
   harmless.
5. **One shared resolution path, async at the command boundary.** The
   assembler must not fork store resolution: it reuses the
   registry-lookup → metadata-check → root-inspection pipeline that
   `resolveStoreRoot` (`src/core/root-selection.ts:134-218`) owns, via
   a non-throwing read-only variant extracted from it — never a
   re-implementation. Because that pipeline is async while
   `generateInstructions` is sync (`instruction-loader.ts:271`), the
   index is assembled by an async core helper invoked from the command
   layer after root resolution, and passed into the (still-sync)
   generators as an input — no async-ification of the instruction
   loader. A registry that cannot be read or parsed at all degrades the
   same way as everything else: each declared reference indexes with a
   `reference_registry_unreadable` warning (fix:
   `openspec store doctor`).
6. **The list is deduplicated, order-preserving, and budgeted like
   context.** A resolved store with zero specs indexes as an entry with
   `specs: []` (the agent learns the store resolved and holds nothing).
   The rendered index shares the spirit of the existing 50KB
   project-context cap (`project-config.ts:45`): if the rendered index
   would exceed 50KB, per-store spec lists are truncated
   (order-preserving) and the entry carries a
   `reference_index_truncated` warning naming the cap — the agent can
   still fetch anything by listing the store directly.
7. **Vocabulary**: the user-facing noun is "referenced store(s)"; the
   JSON field is `references` (matching the config key). No workflow
   template changes in this slice — templates already direct agents to
   read instructions output, and the index is self-describing.
8. **Config parsing keeps raw strings; the assembler validates.**
   `readProjectConfig` accepts `references` as an optional array,
   keeping string-typed entries (deduplicated, order-preserving) and
   dropping only non-strings per the existing resilient style; id
   grammar is the assembler's job so invalid ids surface as index
   diagnostics instead of being silently dropped at parse time.

## User Experience

A PM keeps requirements in the team store. The app repo declares the
relationship once:

```yaml
# app-repo/openspec/config.yaml
schema: spec-driven
references:
  - team-context
```

A dev tells their agent "write the low-level design for billing
invoicing". The agent runs the instructions command it already uses:

```text
$ openspec instructions design --change billing-rework
...
<referenced_stores>
<!-- Read-only upstream context. Fetch what you need; cite what you use. -->
Store team-context (local-path-redacted
  - billing: Billing must support usage-based invoicing across regions
  - auth-sso: Single sign-on requirements for enterprise tenants
  Fetch: openspec show <spec-id> --type spec --store team-context
</referenced_stores>
```

The agent fetches `openspec show billing --type spec --store
team-context`, writes the design in the app repo's own root, and cites
`team-context/billing` in prose. Nothing redirected the change to the
store; nothing copied the requirement into the repo.

When the store is not registered on this machine, the agent (and the
human) see exactly what to do instead of silently missing context:

```text
<referenced_stores>
Store team-context: not registered on this machine.
  Fix: get a checkout from a teammate and run: openspec store register <path> --id team-context
</referenced_stores>
```

## Scope

In scope:

- **Config**: `references:` (optional array of store ids) in
  `ProjectConfigSchema` (`src/core/project-config.ts:19-41`), parsed
  with the existing resilient field-by-field style; invalid entries
  surface through the index diagnostics, valid entries survive.
- **One shared index assembler** (new module under `src/core/`, e.g.
  `references.ts`): resolve each id through the shared non-throwing
  resolution variant (decision 5), enumerate the referenced root's
  `openspec/specs/`, extract first-line summaries tolerantly
  (decision 2), and emit per-store entries
  `{store_id, root, specs: [{id, summary}], fetch, status: [...]}`
  (`root` absolute; `fetch` the per-store recipe string).
- **Artifact instructions**: `generateInstructions`
  (`src/core/artifact-graph/instruction-loader.ts:271-339`) gains a
  `references` field; human mode prints the `<referenced_stores>` block
  in the fixed position after the (conditional) `<project_context>`
  block (`src/commands/workflow/instructions.ts:171-178`) — when
  `context:` is absent, the references block prints in that same slot.
- **Apply instructions**: `generateApplyInstructions`
  (`src/commands/workflow/instructions.ts:282-381`) gains the same
  field; `printApplyInstructionsText` gains a `### Referenced Stores`
  markdown section in its existing style.
- **Diagnostics**: the five new warning codes above
  (`reference_unresolved`, `reference_invalid_id`,
  `reference_root_unhealthy`, `reference_registry_unreadable`,
  `reference_index_truncated`), in the established shape.
- **Tests**: config parsing (valid, dedup, non-string entries dropped,
  raw invalid-grammar strings kept); assembler unit coverage (resolved,
  unresolved, unhealthy incl. missing checkout path, self-reference,
  zero-spec store, missing Purpose, unparseable spec file);
  instructions JSON + human output for both surfaces, including the
  context+references ordering pin and the references-without-context
  placement; an e2e test of the layered flow — app repo with a
  reference, registered store with a spec, `instructions` output
  carries the index, and the printed fetch command runs verbatim
  against the built binary.
- **Docs**: a "Referencing stores from a project" subsection in
  `docs/cli.md`'s Stores section documenting the `references:` config
  key (no such config-key reference exists today — this subsection is
  created, not extended).

Out of scope:

- The fallback `store:` pointer for rootless repos (3.2).
- Canonical remotes in store identity and clone-source hints (3.3).
- Later relationship health in doctor (3.6) — instructions-inline diagnostics
  are this slice's only health surface.
- Any change to root resolution, the `--store` flag, or write paths.
- Inlining spec content, caching the index, or citation enforcement.
- `context:` field changes; docs rewrites beyond `docs/cli.md`'s
  config-reference section gaining the `references:` key.

## Acceptance Criteria

### The Declaration

#### Scenario: References Parse Resiliently

- **GIVEN** `openspec/config.yaml` with `references: [team-context,
  team-context, BAD ID, other-context, 7]`
- **WHEN** the config is read
- **THEN** the parsed references are `[team-context, BAD ID,
  other-context]` (deduplicated, order-preserving, string entries only
  — grammar validation is the assembler's job, decision 8)
- **AND** the index output carries a `reference_invalid_id` warning
  naming `BAD ID` with the kebab-grammar fix
- **AND** a config with no `references:` key behaves exactly as today

### The Index

#### Scenario: Instructions Carry The Live Index

- **GIVEN** an app repo whose config references a registered store
  containing specs `billing` and `auth-sso`
- **WHEN** `openspec instructions <artifact> --change <id> --json` runs
  in the app repo
- **THEN** the JSON carries `references: [{store_id: "team-context",
  root: <absolute path>, specs: [{id, summary}, ...], fetch: "openspec
  show <spec-id> --type spec --store team-context"}]`
- **AND** the summaries are the first non-empty Purpose lines, read from
  the store checkout at this moment (editing the store and re-running
  instructions changes the summary — nothing is frozen)
- **AND** spec content is NOT inlined anywhere in the output
- **AND** human mode prints the `<referenced_stores>` block with the
  same information
- **AND** `instructions apply --change <id> --json` carries the same
  `references` field

#### Scenario: The Fetch Recipe Works Verbatim

- **WHEN** the agent runs the printed fetch command with a real spec id
- **THEN** it returns that spec from the store, read-only, while the
  session's own commands keep acting on the app repo's root

#### Scenario: Problems Are Reported, Never Silent

- **GIVEN** a reference to an id absent from the local registry
- **WHEN** instructions run
- **THEN** generation succeeds, and the index entry carries
  `reference_unresolved` (severity `warning`) with a fix naming the
  referenced id: `openspec store register <path> --id <id>`
- **AND** a registered referenced root that is unhealthy — or whose
  checkout path no longer exists on disk — yields
  `reference_root_unhealthy` with the `openspec store doctor <id>` fix
- **AND** when the resolved root IS the referenced store (self
  reference), the entry is omitted with no diagnostic
- **AND** a referenced store's own `references:` are never followed
  (one level deep; A↔B circular declarations cause no recursion)

### The Boundaries Hold

#### Scenario: References Never Move The Root

- **GIVEN** the app repo declares `references: [team-context]`
- **WHEN** `new change`, `status`, `validate`, or `archive` run without
  `--store`
- **THEN** they act on the app repo's own root, byte-identical to a repo
  with no references
- **AND** no command writes anything into the referenced store
- **AND** no per-change link metadata is created anywhere

#### Scenario: Symmetric Declarations

- **GIVEN** a store whose own config carries `references:
  [upstream-context]`
- **WHEN** `instructions ... --store team-context --json` runs
- **THEN** the index reflects `team-context`'s references (resolved
  root's config, not the cwd's)

### The Layered Flow End To End

#### Scenario: PM-To-Dev Journey

- **GIVEN** a registered store with a `billing` spec carrying a Purpose
  section, and an app repo with its own root and a `references`
  declaration
- **WHEN** the e2e test drives: `instructions design --change
  billing-rework --json` in the app repo → reads the index → runs the
  fetch command → writes a design artifact in the app repo citing
  `team-context/billing` → `validate` and `status`
- **THEN** every step succeeds against the built binary
- **AND** the design lands in the app repo's `openspec/changes/`, the
  store is untouched, and the citation is plain prose in the artifact


import signal
import logging
from pathlib import Path
from abc import ABC, abstractmethod

import anyio
from anyio import Path as AsyncPath

from scrapling.spiders.request import Request
from scrapling.spiders.engine import CrawlerEngine
from scrapling.spiders.session import SessionManager
from scrapling.core.utils import set_logger, reset_logger
from scrapling.spiders.result import CrawlResult, CrawlStats
from scrapling.core._types import Set, Any, Dict, Optional, Union, TYPE_CHECKING, AsyncGenerator

BLOCKED_CODES = {401, 403, 407, 429, 444, 500, 502, 503, 504}
if TYPE_CHECKING:
    from scrapling.engines.toolbelt.custom import Response

class LogCounterHandler(logging.Handler):
    """A logging handler that counts log messages by level."""

    def __init__(self):
        super().__init__()
        self.counts = {
            logging.DEBUG: 0,
            logging.INFO: 0,
            logging.WARNING: 0,
            logging.ERROR: 0,
            logging.CRITICAL: 0,
        }

    def emit(self, record: logging.LogRecord) -> None:
        level = record.levelno
        # Map to the closest standard level
        if level >= logging.CRITICAL:
            self.counts[logging.CRITICAL] += 1
        elif level >= logging.ERROR:
            self.counts[logging.ERROR] += 1
        elif level >= logging.WARNING:
            self.counts[logging.WARNING] += 1
        elif level >= logging.INFO:
            self.counts[logging.INFO] += 1
        else:
            self.counts[logging.DEBUG] += 1

    def get_counts(self) -> Dict[str, int]:
        """Return counts as a dictionary with string keys."""
        return {
            "debug": self.counts[logging.DEBUG],
            "info": self.counts[logging.INFO],
            "warning": self.counts[logging.WARNING],
            "error": self.counts[logging.ERROR],
            "critical": self.counts[logging.CRITICAL],
        }

class SessionConfigurationError(Exception):
    """Raised when session configuration fails."""

    pass

class Spider(ABC):
    """An abstract base class for creating web spiders.

    Check the documentation website for more information.
    """

    name: Optional[str] = None
    start_urls: list[str] = []
    allowed_domains: Set[str] = set()

    # Robots.txt compliance
    robots_txt_obey: bool = False

    # Development mode
    development_mode: bool = False
    development_cache_dir: Optional[str] = None

    # Concurrency settings
    concurrent_requests: int = 4
    concurrent_requests_per_domain: int = 0
    download_delay: float = 0.0
    max_blocked_retries: int = 3

    # Fingerprint adjustments
    fp_include_kwargs: bool = False
    fp_keep_fragments: bool = False
    fp_include_headers: bool = False

    # Logging settings
    logging_level: int = logging.DEBUG
    logging_format: str = "[%(asctime)s]:({spider_name}) %(levelname)s: %(message)s"
    logging_date_format: str = "%Y-%m-%d %H:%M:%S"
    log_file: Optional[str] = None

    def __init__(self, crawldir: Optional[Union[str, Path, AsyncPath]] = None, interval: float = 300.0):
        """Initialize the spider.

        :param crawldir: Directory for checkpoint files. If provided, enables pause/resume.
        :param interval: Seconds between periodic checkpoint saves (default 5 minutes).
        """
        if self.name is None:
            raise ValueError(f"{self.__class__.__name__} must have a name.")

        self.logger = logging.getLogger(f"scrapling.spiders.{self.name}")
        self.logger.setLevel(self.logging_level)
        self.logger.handlers.clear()
        self.logger.propagate = False  # Don't propagate to parent 'scrapling' logger

        formatter = logging.Formatter(
            fmt=self.logging_format.format(spider_name=self.name), datefmt=self.logging_date_format
        )

        # Add a log counter handler to track log counts by level
        self._log_counter = LogCounterHandler()
        self.logger.addHandler(self._log_counter)

        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)
        self.logger.addHandler(console_handler)

        if self.log_file:
            Path(self.log_file).parent.mkdir(parents=True, exist_ok=True)
            file_handler = logging.FileHandler(self.log_file)
            file_handler.setFormatter(formatter)
            self.logger.addHandler(file_handler)

        self.crawldir: Optional[Path] = Path(crawldir) if crawldir else None
        self._interval = interval
        self._engine: Optional[CrawlerEngine] = None
        self._original_sigint_handler: Any = None

        self._session_manager = SessionManager()

        try:
            self.configure_sessions(self._session_manager)
        except Exception as e:
            raise SessionConfigurationError(f"Error in {self.__class__.__name__}.configure_sessions(): {e}") from e

        if len(self._session_manager) == 0:
            raise SessionConfigurationError(f"{self.__class__.__name__}.configure_sessions() did not add any sessions")

        self.logger.info("Spider initialized")

    async def start_requests(self) -> AsyncGenerator[Request, None]:
        """Generate initial requests to start the crawl.

        By default, this generates Request objects for each URL in `start_urls`
        using the session manager's default session and `parse()` as callback.

        Override this method for more control over initial requests
        (e.g., to add custom headers, use different callbacks, etc.)
        """
        if not self.start_urls:
            raise RuntimeError(
                "Spider has no starting point, either set `start_urls` or override `start_requests` function."
            )

        for url in self.start_urls:
            yield Request(url, sid=self._session_manager.default_session_id)

    @abstractmethod
    async def parse(self, response: "Response") -> AsyncGenerator[Dict[str, Any] | Request | None, None]:
        """Default callback for processing responses"""
        raise NotImplementedError(f"{self.__class__.__name__} must implement parse() method")
        yield  # Make this a generator for type checkers

    async def on_start(self, resuming: bool = False) -> None:
        """Called before crawling starts. Override for setup logic.

        :param resuming: It's enabled if the spider is resuming from a checkpoint, left for the user to use.
        """
        if resuming:
            self.logger.debug("Resuming spider from checkpoint")
        else:
            self.logger.debug("Starting spider")

    async def on_close(self) -> None:
        """Called after crawling finishes. Override for cleanup logic."""
        self.logger.debug("Spider closed")

    async def on_error(self, request: Request, error: Exception) -> None:
        """
        Handle request errors for all spider requests.

        Override for custom error handling.
        """
        pass

    async def on_scraped_item(self, item: Dict[str, Any]) -> Dict[str, Any] | None:
        """A hook to be overridden by users to do some processing on scraped items, return `None` to drop the item silently."""
        return item

    async def is_blocked(self, response: "Response") -> bool:
        """Check if the response is blocked. Users should override this for custom detection logic."""
        if response.status in BLOCKED_CODES:
            return True
        return False

    async def retry_blocked_request(self, request: Request, response: "Response") -> Request:
        """Users should override this to prepare the blocked request before retrying, if needed."""
        return request

    def __repr__(self) -> str:
        """String representation of the spider."""
        return f"<{self.__class__.__name__} '{self.name}'>"

    def configure_sessions(self, manager: SessionManager) -> None:
        """Configure sessions for this spider.

        Override this method to add custom sessions.
        The default implementation creates a FetcherSession session.

        The first session added becomes the default for `start_requests()` unless specified otherwise.

        :param manager: SessionManager to configure
        """
        from scrapling.fetchers import FetcherSession

        manager.add("default", FetcherSession())

    def pause(self):
        """Request graceful shutdown of the crawling process."""
        if self._engine:
            self._engine.request_pause()
        else:
            raise RuntimeError("No active crawl to stop")

    def _setup_signal_handler(self) -> None:
        """Set up SIGINT handler for graceful pause."""

        def handler(_signum: int, _frame: Any) -> None:
            if self._engine:
                self._engine.request_pause()
            else:
                # No engine yet, just raise KeyboardInterrupt
                raise KeyboardInterrupt

        try:
            self._original_sigint_handler = signal.signal(signal.SIGINT, handler)
        except ValueError:
            self._original_sigint_handler = None

    def _restore_signal_handler(self) -> None:
        """Restore original SIGINT handler."""
        if self._original_sigint_handler is not None:
            try:
                signal.signal(signal.SIGINT, self._original_sigint_handler)
            except ValueError:
                pass

    async def __run(self) -> CrawlResult:
        token = set_logger(self.logger)
        try:
            self._engine = CrawlerEngine(self, self._session_manager, self.crawldir, self._interval)
            stats = await self._engine.crawl()
            paused = self._engine.paused
            return CrawlResult(stats=stats, items=self._engine.items, paused=paused)
        finally:
            self._engine = None
            reset_logger(token)
            # Close any file handlers to release file resources.
            if self.log_file:
                for handler in self.logger.handlers:
                    if isinstance(handler, logging.FileHandler):
                        handler.close()

    def start(self, use_uvloop: bool = False, **backend_options: Any) -> CrawlResult:
        """Run the spider and return results.

        This is the main entry point for running a spider.
        Handles async execution internally via anyio.

        Pressing Ctrl+C will initiate graceful shutdown (waits for active tasks to complete).
        Pressing Ctrl+C a second time will force immediate stop.

        If crawldir is set, a checkpoint will also be saved on graceful shutdown,
        allowing you to resume the crawl later by running the spider again.

        :param use_uvloop: Whether to use the faster uvloop/winloop event loop implementation, if available.
        :param backend_options: Asyncio backend options to be used with `anyio.run`
        """
        backend_options = backend_options or {}
        if use_uvloop:
            backend_options.update({"use_uvloop": True})

        # Set up SIGINT handler for graceful shutdown
        self._setup_signal_handler()
        try:
            return anyio.run(self.__run, backend="asyncio", backend_options=backend_options)
        finally:
            self._restore_signal_handler()

    async def stream(self) -> AsyncGenerator[Dict[str, Any], None]:
        """Stream items as they're scraped. Ideal for long-running spiders or building applications on top of the spiders.

        Must be called from an async context. Yields items one by one as they are scraped.
        Access `spider.stats` during iteration for real-time statistics.

        Note: SIGINT handling for pause/resume is not available in stream mode.
        """
        token = set_logger(self.logger)
        try:
            self._engine = CrawlerEngine(self, self._session_manager, self.crawldir, self._interval)
            async for item in self._engine:
                yield item
        finally:
            self._engine = None
            reset_logger(token)
            if self.log_file:
                for handler in self.logger.handlers:
                    if isinstance(handler, logging.FileHandler):
                        handler.close()

    @property
    def stats(self) -> CrawlStats:
        """Access current crawl stats (works during streaming)."""
        if self._engine:
            return self._engine.stats
        raise RuntimeError("No active crawl. Use this property inside `async for item in spider.stream():`")


/*
 * Copyright 2025 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package filesystem

import (
	"context"
	"fmt"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"
)

func TestInMemoryBackend_WriteAndRead(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	// Test Write
	err := backend.Write(ctx, &WriteRequest{
		FilePath: "/test.txt",
		Content:  "line1\nline2\nline3\nline4\nline5",
	})
	if err != nil {
		t.Fatalf("Write failed: %v", err)
	}

	// Test Read - full content
	content, err := backend.Read(ctx, &ReadRequest{
		FilePath: "/test.txt",
		Limit:    100,
	})
	if err != nil {
		t.Fatalf("Read failed: %v", err)
	}
	expected := "line1\nline2\nline3\nline4\nline5"
	if content.Content != expected {
		t.Errorf("Read content mismatch. Expected: %q, Got: %q", expected, content.Content)
	}

	// Test Read - with offset and limit
	content, err = backend.Read(ctx, &ReadRequest{
		FilePath: "/test.txt",
		Offset:   1,
		Limit:    2,
	})
	if err != nil {
		t.Fatalf("Read with offset failed: %v", err)
	}
	expected = "line1\nline2"
	if content.Content != expected {
		t.Errorf("Read with offset content mismatch. Expected: %q, Got: %q", expected, content.Content)
	}

	// Test Read - non-existent file
	_, err = backend.Read(ctx, &ReadRequest{
		FilePath: "/nonexistent.txt",
		Limit:    10,
	})
	if err == nil {
		t.Error("Expected error for non-existent file, got nil")
	}
}

func TestInMemoryBackend_LsInfo(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	// Create some files
	backend.Write(ctx, &WriteRequest{
		FilePath: "/file1.txt",
		Content:  "content1",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/file2.txt",
		Content:  "content2",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/file3.txt",
		Content:  "content3",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/subdir/file4.txt",
		Content:  "content4",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir2/file5.txt",
		Content:  "content5",
	})

	// Test LsInfo - root
	infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
	if err != nil {
		t.Fatalf("LsInfo failed: %v", err)
	}
	if len(infos) != 4 { // file1.txt, file2.txt, dir1, dir2
		t.Errorf("Expected 4 items in root, got %d", len(infos))
	}

	// Test LsInfo - specific directory
	infos, err = backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"})
	if err != nil {
		t.Fatalf("LsInfo for /dir1 failed: %v", err)
	}
	if len(infos) != 2 { // file3.txt, subdir
		t.Errorf("Expected 2 items in /dir1, got %d", len(infos))
	}
}

func TestInMemoryBackend_Edit(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	// Create a file
	backend.Write(ctx, &WriteRequest{
		FilePath: "/edit.txt",
		Content:  "hello world\nhello again\nhello world",
	})

	// Test Edit - report error if old string occurs
	err := backend.Edit(ctx, &EditRequest{
		FilePath:   "/edit.txt",
		OldString:  "hello",
		NewString:  "hi",
		ReplaceAll: false,
	})
	if err == nil {
		t.Fatal("should have failed")
	}

	// Test Edit - replace all occurrences
	backend.Write(ctx, &WriteRequest{
		FilePath: "/edit2.txt",
		Content:  "hello world\nhello again\nhello world",
	})
	err = backend.Edit(ctx, &EditRequest{
		FilePath:   "/edit2.txt",
		OldString:  "hello",
		NewString:  "hi",
		ReplaceAll: true,
	})
	if err != nil {
		t.Fatalf("Edit (replace all) failed: %v", err)
	}

	content, _ := backend.Read(ctx, &ReadRequest{
		FilePath: "/edit2.txt",
		Limit:    100,
	})
	expected := "hi world\nhi again\nhi world"
	if content.Content != expected {
		t.Errorf("Edit (replace all) content mismatch. Expected: %q, Got: %q", expected, content.Content)
	}

	// Test Edit - non-existent file
	err = backend.Edit(ctx, &EditRequest{
		FilePath:   "/nonexistent.txt",
		OldString:  "old",
		NewString:  "new",
		ReplaceAll: false,
	})
	if err == nil {
		t.Error("Expected error for non-existent file, got nil")
	}

	// Test Edit - empty oldString
	err = backend.Edit(ctx, &EditRequest{
		FilePath:   "/edit.txt",
		OldString:  "",
		NewString:  "new",
		ReplaceAll: false,
	})
	if err == nil {
		t.Error("Expected error for empty oldString, got nil")
	}
}

func TestInMemoryBackend_LsInfo_PathIsFilename(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/file1.txt",
		Content:  "content1",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/file2.txt",
		Content:  "content2",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/file3.txt",
		Content:  "content3",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/subdir/file4.txt",
		Content:  "content4",
	})

	t.Run("RootDirectory", func(t *testing.T) {
		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		for _, info := range infos {
			if strings.Contains(info.Path, "/") {
				t.Errorf("Path should be filename only, got: %s", info.Path)
			}
			if info.IsDir {
				if info.Path != "dir1" {
					t.Errorf("Expected directory name 'dir1', got: %s", info.Path)
				}
			} else {
				if info.Path != "file1.txt" && info.Path != "file2.txt" {
					t.Errorf("Expected filename 'file1.txt' or 'file2.txt', got: %s", info.Path)
				}
			}
		}
	})

	t.Run("Subdirectory", func(t *testing.T) {
		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		for _, info := range infos {
			if strings.Contains(info.Path, "/") {
				t.Errorf("Path should be filename only, got: %s", info.Path)
			}
			if info.IsDir {
				if info.Path != "subdir" {
					t.Errorf("Expected directory name 'subdir', got: %s", info.Path)
				}
			} else {
				if info.Path != "file3.txt" {
					t.Errorf("Expected filename 'file3.txt', got: %s", info.Path)
				}
			}
		}
	})

	t.Run("NestedSubdirectory", func(t *testing.T) {
		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1/subdir"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos))
		}

		info := infos[0]
		if info.Path != "file4.txt" {
			t.Errorf("Expected filename 'file4.txt', got: %s", info.Path)
		}
		if strings.Contains(info.Path, "/") {
			t.Errorf("Path should be filename only, got: %s", info.Path)
		}
	})
}

func TestInMemoryBackend_GlobInfo(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	// Create some files
	backend.Write(ctx, &WriteRequest{
		FilePath: "/file1.txt",
		Content:  "content1",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/file2.py",
		Content:  "content2",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/file3.txt",
		Content:  "content3",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir1/file4.py",
		Content:  "content4",
	})

	// Test GlobInfo - match .txt files in root only
	infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
		Pattern: "*.txt",
		Path:    "/",
	})
	if err != nil {
		t.Fatalf("GlobInfo failed: %v", err)
	}
	if len(infos) != 1 { // only file1.txt in root
		t.Errorf("Expected 1 .txt file in root, got %d", len(infos))
	}
	if infos[0].Path != "file1.txt" {
		t.Errorf("Expected relative path 'file1.txt', got %s", infos[0].Path)
	}

	// Test GlobInfo - match all .py files in dir1
	infos, err = backend.GlobInfo(ctx, &GlobInfoRequest{
		Pattern: "*.py",
		Path:    "/dir1",
	})
	if err != nil {
		t.Fatalf("GlobInfo for /dir1 failed: %v", err)
	}
	if len(infos) != 1 { // file4.py
		t.Errorf("Expected 1 .py file in /dir1, got %d", len(infos))
	}
	if infos[0].Path != "file4.py" {
		t.Errorf("Expected relative path 'file4.py', got %s", infos[0].Path)
	}
}

func TestInMemoryBackend_GlobInfo_RelativePath(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "local-path-redacted",
		Content:  "content1",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "local-path-redacted",
		Content:  "content2",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "local-path-redacted",
		Content:  "content3",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "local-path-redacted",
		Content:  "content4",
	})

	t.Run("GlobFromRootWithPattern", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "**/*.go",
			Path:    "local-path-redacted",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 3 {
			t.Fatalf("Expected 3 .go files, got %d", len(infos))
		}

		expectedPaths := map[string]bool{
			"eino/file1.go":                      false,
			"openai-go/paginationmanual_test.go": false,
			"openai-go/paginationauto_test.go":   false,
		}

		for _, info := range infos {
			if _, exists := expectedPaths[info.Path]; exists {
				expectedPaths[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expectedPaths {
			if !found {
				t.Errorf("Expected path not found: %s", path)
			}
		}
	})

	t.Run("GlobFromSubdirectory", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "*.go",
			Path:    "local-path-redacted",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 2 {
			t.Fatalf("Expected 2 .go files, got %d", len(infos))
		}

		expectedPaths := map[string]bool{
			"paginationmanual_test.go": false,
			"paginationauto_test.go":   false,
		}

		for _, info := range infos {
			if _, exists := expectedPaths[info.Path]; exists {
				expectedPaths[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expectedPaths {
			if !found {
				t.Errorf("Expected path not found: %s", path)
			}
		}
	})

	t.Run("GlobFromRootWithAbsolutePattern", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "local-path-redacted",
			Path:    "/",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		expected := map[string]bool{
			"local-path-redacted":                      false,
			"local-path-redacted": false,
			"local-path-redacted":   false,
		}
		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			}
		}
		for path, found := range expected {
			if !found {
				t.Errorf("Expected absolute path not found: %s", path)
			}
		}
	})

	t.Run("GlobRecursiveWithRelativePattern", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "**/*.go",
			Path:    "local-path-redacted",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 3 {
			t.Fatalf("Expected 3 .go files with ** pattern, got %d", len(infos))
		}

		expected := map[string]bool{
			"eino/file1.go":                      false,
			"openai-go/paginationmanual_test.go": false,
			"openai-go/paginationauto_test.go":   false,
		}

		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expected {
			if !found {
				t.Errorf("Expected relative path not found: %s", path)
			}
		}
	})
}

func TestInMemoryBackend_GlobInfo_RecursivePattern(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/src/main.go",
		Content:  "main",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/src/utils/helper.go",
		Content:  "helper",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/src/utils/deep/nested.go",
		Content:  "nested",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/test/test.go",
		Content:  "test",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/README.md",
		Content:  "readme",
	})

	t.Run("DoubleStarMatchesAllSubdirectories", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "**/*.go",
			Path:    "/project",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 4 {
			t.Fatalf("Expected 4 .go files, got %d", len(infos))
		}

		expected := map[string]bool{
			"src/main.go":              false,
			"src/utils/helper.go":      false,
			"src/utils/deep/nested.go": false,
			"test/test.go":             false,
		}

		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expected {
			if !found {
				t.Errorf("Expected path not found: %s", path)
			}
		}
	})

	t.Run("DoubleStarInMiddleOfPattern", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "src/**/*.go",
			Path:    "/project",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 3 {
			t.Fatalf("Expected 3 .go files under src/, got %d", len(infos))
		}

		expected := map[string]bool{
			"src/main.go":              false,
			"src/utils/helper.go":      false,
			"src/utils/deep/nested.go": false,
		}

		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expected {
			if !found {
				t.Errorf("Expected path not found: %s", path)
			}
		}
	})

	t.Run("DoubleStarAtEnd", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "src/**",
			Path:    "/project",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 3 {
			t.Fatalf("Expected 3 files under src/, got %d", len(infos))
		}

		expected := map[string]bool{
			"src/main.go":              false,
			"src/utils/helper.go":      false,
			"src/utils/deep/nested.go": false,
		}

		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			}
		}

		for path, found := range expected {
			if !found {
				t.Errorf("Expected path not found: %s", path)
			}
		}
	})

	t.Run("AbsolutePatternWithDoubleStarRecursive", func(t *testing.T) {
		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "/project/**/*.go",
			Path:    "/",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 4 {
			t.Fatalf("Expected 4 .go files, got %d", len(infos))
		}

		expected := map[string]bool{
			"/project/src/main.go":              false,
			"/project/src/utils/helper.go":      false,
			"/project/src/utils/deep/nested.go": false,
			"/project/test/test.go":             false,
		}

		for _, info := range infos {
			if _, ok := expected[info.Path]; ok {
				expected[info.Path] = true
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}

		for path, found := range expected {
			if !found {
				t.Errorf("Expected absolute path not found: %s", path)
			}
		}
	})
}

func TestInMemoryBackend_Concurrent(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	// Test concurrent writes and reads
	done := make(chan bool)
	for i := 0; i < 10; i++ {
		go func(n int) {
			backend.Write(ctx, &WriteRequest{
				FilePath: "/concurrent.txt",
				Content:  "content",
			})
			backend.Read(ctx, &ReadRequest{
				FilePath: "/concurrent.txt",
				Limit:    10,
			})
			done <- true
		}(i)
	}

	for i := 0; i < 10; i++ {
		<-done
	}
}

func TestInMemoryBackend_LsInfo_FileInfoMetadata(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	t.Run("FileMetadata", func(t *testing.T) {
		content := "hello world"
		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  content,
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos))
		}

		info := infos[0]
		if info.Path != "test.txt" {
			t.Errorf("Expected path test.txt, got %s", info.Path)
		}
		if info.IsDir {
			t.Error("Expected IsDir to be false for file")
		}
		if info.Size != int64(len(content)) {
			t.Errorf("Expected size %d, got %d", len(content), info.Size)
		}
		if info.ModifiedAt == "" {
			t.Error("Expected ModifiedAt to be non-empty")
		}
		_, err = time.Parse(time.RFC3339Nano, info.ModifiedAt)
		if err != nil {
			t.Errorf("ModifiedAt is not valid RFC3339 format: %v", err)
		}
	})

	t.Run("DirectoryMetadata", func(t *testing.T) {
		backend := NewInMemoryBackend()
		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/file1.txt",
			Content:  "content1",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 1 {
			t.Fatalf("Expected 1 directory, got %d", len(infos))
		}

		info := infos[0]
		if info.Path != "dir1" {
			t.Errorf("Expected path dir1, got %s", info.Path)
		}
		if !info.IsDir {
			t.Error("Expected IsDir to be true for directory")
		}
		if info.Size != 0 {
			t.Errorf("Expected size 0 for directory, got %d", info.Size)
		}
		if info.ModifiedAt == "" {
			t.Error("Expected ModifiedAt to be non-empty for directory")
		}
	})

	t.Run("MixedFilesAndDirectories", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{
			FilePath: "/file1.txt",
			Content:  "content1",
		})
		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/file2.txt",
			Content:  "content2",
		})
		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/subdir/file3.txt",
			Content:  "content3",
		})

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 2 {
			t.Fatalf("Expected 2 items (file1.txt, dir1), got %d", len(infos))
		}

		fileCount := 0
		dirCount := 0
		for _, info := range infos {
			if info.IsDir {
				dirCount++
				if info.Path != "dir1" {
					t.Errorf("Expected directory path dir1, got %s", info.Path)
				}
			} else {
				fileCount++
				if info.Path != "file1.txt" {
					t.Errorf("Expected file path file1.txt, got %s", info.Path)
				}
				if info.Size != int64(len("content1")) {
					t.Errorf("Expected file size %d, got %d", len("content1"), info.Size)
				}
			}
		}

		if fileCount != 1 {
			t.Errorf("Expected 1 file, got %d", fileCount)
		}
		if dirCount != 1 {
			t.Errorf("Expected 1 directory, got %d", dirCount)
		}
	})

	t.Run("SubdirectoryListing", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/file1.txt",
			Content:  "short",
		})
		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/subdir/file2.txt",
			Content:  "longer content here",
		})

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 2 {
			t.Fatalf("Expected 2 items (file1.txt, subdir), got %d", len(infos))
		}

		for _, info := range infos {
			if info.Path == "file1.txt" {
				if info.IsDir {
					t.Error("Expected file1.txt to be a file")
				}
				if info.Size != int64(len("short")) {
					t.Errorf("Expected size %d, got %d", len("short"), info.Size)
				}
			} else if info.Path == "subdir" {
				if !info.IsDir {
					t.Error("Expected subdir to be a directory")
				}
			} else {
				t.Errorf("Unexpected path: %s", info.Path)
			}
		}
	})

	t.Run("DirectoryModifiedAtUsesLatestFile", func(t *testing.T) {
		backend := NewInMemoryBackend()

		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/file1.txt",
			Content:  "content1",
		})
		time.Sleep(10 * time.Millisecond)

		backend.Write(ctx, &WriteRequest{
			FilePath: "/dir1/file2.txt",
			Content:  "content2",
		})

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}

		if len(infos) != 1 {
			t.Fatalf("Expected 1 directory, got %d", len(infos))
		}

		dirInfo := infos[0]
		if !dirInfo.IsDir {
			t.Fatal("Expected directory")
		}

		dirModTime, _ := time.Parse(time.RFC3339Nano, dirInfo.ModifiedAt)

		subInfos, _ := backend.LsInfo(ctx, &LsInfoRequest{Path: "/dir1"})
		var latestFileTime time.Time
		for _, info := range subInfos {
			fileTime, _ := time.Parse(time.RFC3339Nano, info.ModifiedAt)
			if fileTime.After(latestFileTime) {
				latestFileTime = fileTime
			}
		}

		if !dirModTime.Equal(latestFileTime) && dirModTime.Before(latestFileTime) {
			t.Logf("Directory mod time: %v, Latest file time: %v", dirModTime, latestFileTime)
		}
	})
}

func TestInMemoryBackend_GlobInfo_FileInfoMetadata(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	t.Run("BasicMetadata", func(t *testing.T) {
		content := "test content"
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  content,
		})

		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "*.txt",
			Path:    "/",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos))
		}

		info := infos[0]
		if info.Path != "test.txt" {
			t.Errorf("Expected path test.txt, got %s", info.Path)
		}
		if info.IsDir {
			t.Error("Expected IsDir to be false")
		}
		if info.Size != int64(len(content)) {
			t.Errorf("Expected size %d, got %d", len(content), info.Size)
		}
		if info.ModifiedAt == "" {
			t.Error("Expected ModifiedAt to be non-empty")
		}
	})

	t.Run("MultipleFilesMetadata", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{
			FilePath: "/file1.txt",
			Content:  "short",
		})
		backend.Write(ctx, &WriteRequest{
			FilePath: "/file2.txt",
			Content:  "much longer content",
		})
		backend.Write(ctx, &WriteRequest{
			FilePath: "/file3.py",
			Content:  "python",
		})

		infos, err := backend.GlobInfo(ctx, &GlobInfoRequest{
			Pattern: "*.txt",
			Path:    "/",
		})
		if err != nil {
			t.Fatalf("GlobInfo failed: %v", err)
		}

		if len(infos) != 2 {
			t.Fatalf("Expected 2 .txt files, got %d", len(infos))
		}

		for _, info := range infos {
			if info.IsDir {
				t.Errorf("Expected IsDir to be false for %s", info.Path)
			}
			if info.Size <= 0 {
				t.Errorf("Expected positive size for %s, got %d", info.Path, info.Size)
			}
			if info.ModifiedAt == "" {
				t.Errorf("Expected ModifiedAt to be non-empty for %s", info.Path)
			}
		}
	})
}

func TestInMemoryBackend_WriteAndEdit_ModifiedAt(t *testing.T) {
	ctx := context.Background()

	t.Run("WriteUpdatesModifiedAt", func(t *testing.T) {
		backend := NewInMemoryBackend()
		beforeWrite := time.Now()
		time.Sleep(1 * time.Millisecond)

		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "initial content",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		time.Sleep(1 * time.Millisecond)
		afterWrite := time.Now()

		infos, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos))
		}

		modTime, err := time.Parse(time.RFC3339Nano, infos[0].ModifiedAt)
		if err != nil {
			t.Fatalf("Failed to parse ModifiedAt: %v", err)
		}

		if modTime.Before(beforeWrite) || modTime.After(afterWrite) {
			t.Errorf("ModifiedAt %v should be between %v and %v", modTime, beforeWrite, afterWrite)
		}
	})

	t.Run("EditUpdatesModifiedAt", func(t *testing.T) {
		backend := NewInMemoryBackend()
		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/edit.txt",
			Content:  "hello world",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos1) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos1))
		}
		modTime1, err := time.Parse(time.RFC3339Nano, infos1[0].ModifiedAt)
		if err != nil {
			t.Fatalf("Failed to parse ModifiedAt: %v", err)
		}

		time.Sleep(10 * time.Millisecond)

		err = backend.Edit(ctx, &EditRequest{
			FilePath:   "/edit.txt",
			OldString:  "hello",
			NewString:  "hi",
			ReplaceAll: true,
		})
		if err != nil {
			t.Fatalf("Edit failed: %v", err)
		}

		infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos2) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos2))
		}
		modTime2, err := time.Parse(time.RFC3339Nano, infos2[0].ModifiedAt)
		if err != nil {
			t.Fatalf("Failed to parse ModifiedAt: %v", err)
		}

		if !modTime2.After(modTime1) {
			t.Errorf("ModifiedAt should be updated after edit. Before: %v, After: %v", modTime1, modTime2)
		}
	})

	t.Run("OverwriteUpdatesModifiedAt", func(t *testing.T) {
		backend := NewInMemoryBackend()
		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/overwrite.txt",
			Content:  "original",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos1) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos1))
		}
		modTime1, err := time.Parse(time.RFC3339Nano, infos1[0].ModifiedAt)
		if err != nil {
			t.Fatalf("Failed to parse ModifiedAt: %v", err)
		}

		time.Sleep(10 * time.Millisecond)

		err = backend.Write(ctx, &WriteRequest{
			FilePath: "/overwrite.txt",
			Content:  "new content",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos2) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos2))
		}
		modTime2, err := time.Parse(time.RFC3339Nano, infos2[0].ModifiedAt)
		if err != nil {
			t.Fatalf("Failed to parse ModifiedAt: %v", err)
		}

		if !modTime2.After(modTime1) {
			t.Errorf("ModifiedAt should be updated after overwrite. Before: %v, After: %v", modTime1, modTime2)
		}
	})

	t.Run("SizeUpdatesAfterEdit", func(t *testing.T) {
		backend := NewInMemoryBackend()
		err := backend.Write(ctx, &WriteRequest{
			FilePath: "/size.txt",
			Content:  "hello",
		})
		if err != nil {
			t.Fatalf("Write failed: %v", err)
		}

		infos1, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos1) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos1))
		}
		size1 := infos1[0].Size

		err = backend.Edit(ctx, &EditRequest{
			FilePath:   "/size.txt",
			OldString:  "hello",
			NewString:  "hello world",
			ReplaceAll: true,
		})
		if err != nil {
			t.Fatalf("Edit failed: %v", err)
		}

		infos2, err := backend.LsInfo(ctx, &LsInfoRequest{Path: "/"})
		if err != nil {
			t.Fatalf("LsInfo failed: %v", err)
		}
		if len(infos2) != 1 {
			t.Fatalf("Expected 1 file, got %d", len(infos2))
		}
		size2 := infos2[0].Size

		if size2 <= size1 {
			t.Errorf("Size should increase after edit. Before: %d, After: %d", size1, size2)
		}
		if size2 != int64(len("hello world")) {
			t.Errorf("Expected size %d, got %d", len("hello world"), size2)
		}
	})
}

func TestInMemoryBackend_Read_EdgeCases(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/test.txt",
		Content:  "line1\nline2\nline3",
	})

	t.Run("negative offset should be treated as zero", func(t *testing.T) {
		content, err := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Offset:   -5,
			Limit:    2,
		})
		if err != nil {
			t.Fatalf("Read failed: %v", err)
		}
		expected := "line1\nline2"
		if content.Content != expected {
			t.Errorf("Expected: %q, Got: %q", expected, content.Content)
		}
	})

	t.Run("offset exceeds file length", func(t *testing.T) {
		content, err := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Offset:   100,
			Limit:    10,
		})
		if err != nil {
			t.Fatalf("Read failed: %v", err)
		}
		if content.Content != "" {
			t.Errorf("Expected empty content, got: %q", content.Content)
		}
	})

	t.Run("zero or negative limit should use default 200", func(t *testing.T) {
		content, err := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Offset:   0,
			Limit:    0,
		})
		if err != nil {
			t.Fatalf("Read failed: %v", err)
		}
		lines := strings.Split(content.Content, "\n")
		if len(lines) != 3 {
			t.Errorf("Expected 3 lines, got %d", len(lines))
		}
	})

	t.Run("limit exceeds remaining lines", func(t *testing.T) {
		content, err := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Offset:   1,
			Limit:    100,
		})
		if err != nil {
			t.Fatalf("Read failed: %v", err)
		}
		lines := strings.Split(content.Content, "\n")
		if len(lines) != 3 {
			t.Errorf("Expected 3 lines, got %d", len(lines))
		}
	})
}

func TestInMemoryBackend_Edit_EdgeCases(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	t.Run("edit non-existent file", func(t *testing.T) {
		err := backend.Edit(ctx, &EditRequest{
			FilePath:  "/nonexistent.txt",
			OldString: "old",
			NewString: "new",
		})
		if err == nil {
			t.Error("Expected error for non-existent file")
		}
		if !strings.Contains(err.Error(), "not found") {
			t.Errorf("Expected 'not found' error, got: %v", err)
		}
	})

	t.Run("empty oldString", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "content",
		})

		err := backend.Edit(ctx, &EditRequest{
			FilePath:  "/test.txt",
			OldString: "",
			NewString: "new",
		})
		if err == nil {
			t.Error("Expected error for empty oldString")
		}
		if !strings.Contains(err.Error(), "non-empty") {
			t.Errorf("Expected 'non-empty' error, got: %v", err)
		}
	})

	t.Run("oldString not found", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "hello world",
		})

		err := backend.Edit(ctx, &EditRequest{
			FilePath:  "/test.txt",
			OldString: "notfound",
			NewString: "new",
		})
		if err == nil {
			t.Error("Expected error when oldString not found")
		}
		if !strings.Contains(err.Error(), "not found in file") {
			t.Errorf("Expected 'not found in file' error, got: %v", err)
		}
	})

	t.Run("multiple occurrences with ReplaceAll false", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "foo bar foo baz",
		})

		err := backend.Edit(ctx, &EditRequest{
			FilePath:   "/test.txt",
			OldString:  "foo",
			NewString:  "FOO",
			ReplaceAll: false,
		})
		if err == nil {
			t.Error("Expected error for multiple occurrences with ReplaceAll=false")
		}
		if !strings.Contains(err.Error(), "multiple occurrences") {
			t.Errorf("Expected 'multiple occurrences' error, got: %v", err)
		}
	})

	t.Run("single occurrence with ReplaceAll false", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "foo bar baz",
		})

		err := backend.Edit(ctx, &EditRequest{
			FilePath:   "/test.txt",
			OldString:  "foo",
			NewString:  "FOO",
			ReplaceAll: false,
		})
		if err != nil {
			t.Fatalf("Edit failed: %v", err)
		}

		content, _ := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Limit:    100,
		})
		if !strings.Contains(content.Content, "FOO") {
			t.Error("Expected content to contain 'FOO'")
		}
	})

	t.Run("ReplaceAll replaces all occurrences", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/test.txt",
			Content:  "foo bar foo baz foo",
		})

		err := backend.Edit(ctx, &EditRequest{
			FilePath:   "/test.txt",
			OldString:  "foo",
			NewString:  "FOO",
			ReplaceAll: true,
		})
		if err != nil {
			t.Fatalf("Edit failed: %v", err)
		}

		content, _ := backend.Read(ctx, &ReadRequest{
			FilePath: "/test.txt",
			Limit:    100,
		})
		if strings.Contains(content.Content, "foo") {
			t.Error("Expected all 'foo' to be replaced")
		}
		fooCount := strings.Count(content.Content, "FOO")
		if fooCount != 3 {
			t.Errorf("Expected 3 occurrences of 'FOO', got %d", fooCount)
		}
	})
}

func TestInMemoryBackend_NormalizePath(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	t.Run("paths are normalized on write", func(t *testing.T) {
		testCases := []struct {
			inputPath      string
			normalizedPath string
		}{
			{"test.txt", "/test.txt"},
			{"/test.txt", "/test.txt"},
			{"//test.txt", "/test.txt"},
			{"/dir//file.txt", "/dir/file.txt"},
			{"/dir/../file.txt", "/file.txt"},
		}

		for _, tc := range testCases {
			backend.Write(ctx, &WriteRequest{
				FilePath: tc.inputPath,
				Content:  "content",
			})

			content, err := backend.Read(ctx, &ReadRequest{
				FilePath: tc.normalizedPath,
				Limit:    10,
			})
			if err != nil {
				t.Errorf("Failed to read normalized path %s (from %s): %v", tc.normalizedPath, tc.inputPath, err)
			}
			if !strings.Contains(content.Content, "content") {
				t.Errorf("Content not found for normalized path %s (from %s)", tc.normalizedPath, tc.inputPath)
			}
		}
	})
}

func TestInMemoryBackend_MatchFileType(t *testing.T) {
	testCases := []struct {
		ext      string
		fileType string
		expected bool
	}{
		{"go", "go", true},
		{"py", "python", true},
		{"py", "py", true},
		{"js", "js", true},
		{"ts", "typescript", true},
		{"ts", "ts", true},
		{"cpp", "cpp", true},
		{"c", "c", true},
		{"h", "c", true},
		{"md", "markdown", true},
		{"txt", "txt", true},
		{"go", "python", false},
		{"js", "typescript", false},
		{"unknown", "go", false},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("%s matches %s", tc.ext, tc.fileType), func(t *testing.T) {
			result := matchFileType(tc.ext, tc.fileType)
			if result != tc.expected {
				t.Errorf("matchFileType(%q, %q) = %v, expected %v", tc.ext, tc.fileType, result, tc.expected)
			}
		})
	}
}

func TestInMemoryBackend_GrepRaw(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/test.go",
		Content:  "package main\nfunc main() {\n\tlog.Error(\"error\")\n\tfmt.Println(\"hello\")\n}",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/test.py",
		Content:  "def hello():\n    print('error')\n    print('world')",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/dir/file.go",
		Content:  "package test\nfunc TestError() {\n\tlog.Error(\"test error\")\n}",
	})

	t.Run("basic pattern search", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "error",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 3 {
			t.Errorf("Expected 2 matches, got %d", len(matches))
		}
	})

	t.Run("empty pattern error", func(t *testing.T) {
		_, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "",
		})
		if err == nil {
			t.Error("Expected error for empty pattern")
		}
		if !strings.Contains(err.Error(), "cannot be empty") {
			t.Errorf("Expected 'cannot be empty' error, got: %v", err)
		}
	})

	t.Run("invalid regex pattern", func(t *testing.T) {
		_, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "[invalid",
		})
		if err == nil {
			t.Error("Expected error for invalid regex")
		}
		if !strings.Contains(err.Error(), "invalid regex") {
			t.Errorf("Expected 'invalid regex' error, got: %v", err)
		}
	})

	t.Run("case sensitive search", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "Error",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 3 {
			t.Errorf("Expected 2 matches, got %d", len(matches))
		}
	})

	t.Run("case insensitive search", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "ERROR",
			CaseInsensitive: true,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 3 {
			t.Errorf("Expected at least 2 matches, got %d", len(matches))
		}
	})

	t.Run("filter by file type", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:  "error",
			FileType: "go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasSuffix(match.Path, ".go") {
				t.Errorf("Expected only .go files, got: %s", match.Path)
			}
		}
	})

	t.Run("filter by glob pattern", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "Error",
			Glob:    "*.go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasSuffix(match.Path, ".go") {
				t.Errorf("Expected only .go files, got: %s", match.Path)
			}
		}
	})

	t.Run("invalid glob pattern", func(t *testing.T) {
		_, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "error",
			Glob:    "[invalid",
		})
		if err == nil {
			t.Error("Expected error for invalid glob pattern")
		}
		if !strings.Contains(err.Error(), "invalid glob") {
			t.Errorf("Expected 'invalid glob' error, got: %v", err)
		}
	})

	t.Run("search in specific path", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "Error",
			Path:    "/dir",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasPrefix(match.Path, "/dir") {
				t.Errorf("Expected matches only from /dir, got: %s", match.Path)
			}
		}
	})

	t.Run("search with non-existent path", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "error",
			Path:    "/nonexistent",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches for non-existent path, got %d", len(matches))
		}
	})

	t.Run("regex pattern matching", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "log\\..*Error",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 1 {
			t.Errorf("Expected at least 1 match, got %d", len(matches))
		}
	})

	t.Run("no matches found", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "nonexistent_pattern_xyz",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches, got %d", len(matches))
		}
	})

	t.Run("match line numbers", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:  "log\\.Error",
			FileType: "go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if match.Line <= 0 {
				t.Errorf("Expected positive line number, got %d", match.Line)
			}
		}
	})

	t.Run("match content is returned", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "package main",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 1 {
			t.Fatal("Expected at least 1 match")
		}
		found := false
		for _, match := range matches {
			if strings.Contains(match.Content, "package main") {
				found = true
				break
			}
		}
		if !found {
			t.Error("Expected match content to contain 'package main'")
		}
	})
}

func TestInMemoryBackend_GrepRaw_WithContext(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/context.txt",
		Content:  "line1\nline2\ntarget line\nline4\nline5\nline6",
	})

	t.Run("with before context", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:     "target",
			BeforeLines: 2,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 3 {
			t.Errorf("Expected at least 3 matches (2 before + target), got %d", len(matches))
		}
	})

	t.Run("with after context", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:    "target",
			AfterLines: 2,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 3 {
			t.Errorf("Expected at least 3 matches (target + 2 after), got %d", len(matches))
		}
	})

	t.Run("with both before and after context", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:     "target",
			BeforeLines: 1,
			AfterLines:  1,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 3 {
			t.Errorf("Expected at least 3 matches (1 before + target + 1 after), got %d", len(matches))
		}
	})

	t.Run("context at file boundaries", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/boundary.txt",
			Content:  "first line target\nsecond line",
		})
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:     "target",
			Path:        "/boundary.txt",
			BeforeLines: 5,
			AfterLines:  5,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) == 0 {
			t.Error("Expected at least 1 match")
		}
	})

	t.Run("zero context lines", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:     "target",
			BeforeLines: 0,
			AfterLines:  0,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 1 {
			t.Error("Expected at least 1 match")
		}
	})

	t.Run("negative context lines treated as zero", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:     "target",
			BeforeLines: -5,
			AfterLines:  -5,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) < 1 {
			t.Error("Expected at least 1 match")
		}
	})
}

func TestInMemoryBackend_GrepRaw_Multiline(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/multiline.txt",
		Content:  "start\nmiddle line\nend",
	})

	t.Run("single line mode (default)", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "start.*end",
			EnableMultiline: false,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches in single-line mode, got %d", len(matches))
		}
	})

	t.Run("multiline mode enabled", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "start[\\s\\S]*end",
			EnableMultiline: true,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) == 0 {
			t.Error("Expected matches in multiline mode")
		}
	})

	t.Run("multiline with multiple matches", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/multiline2.txt",
			Content:  "block1 start\nblock1 middle\nblock1 end\n\nblock2 start\nblock2 end",
		})
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "start[\\s\\S]*?end",
			Path:            "/multiline2.txt",
			EnableMultiline: true,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) == 0 {
			t.Error("Expected matches in multiline mode")
		}
	})

	t.Run("multiline with multiple matches v2", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/multiline3.txt",
			Content: `
const a = 1;
function calculateTotal(
  items,
  discount
) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

const b = 2;

/*
 * This is a comment
 * spanning multiple lines
 */

class UserService {
  constructor(db) {
    this.db = db;
  }
}
`,
		})
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "function calculateTotal\\([^\\)]*\\)",
			Path:            "/multiline3.txt",
			EnableMultiline: true,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) == 0 {
			t.Error("Expected matches in multiline mode")
		}

		foundLastLine := false
		for _, match := range matches {
			if match.Line == 6 && strings.Contains(match.Content, ") {") {
				foundLastLine = true
				break
			}
		}
		if !foundLastLine {
			t.Error("Expected to find line 5 with ') {' in content")
			for _, match := range matches {
				t.Logf("Line %d: %s", match.Line, match.Content)
			}
		}
	})

}

func TestInMemoryBackend_GrepRaw_EmptyFiles(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	t.Run("search in empty file", func(t *testing.T) {
		backend.Write(ctx, &WriteRequest{
			FilePath: "/empty.txt",
			Content:  "",
		})
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "anything",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches in empty file, got %d", len(matches))
		}
	})

	t.Run("search with no files", func(t *testing.T) {
		emptyBackend := NewInMemoryBackend()
		matches, err := emptyBackend.GrepRaw(ctx, &GrepRequest{
			Pattern: "anything",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches with no files, got %d", len(matches))
		}
	})
}

func TestInMemoryBackend_GrepRaw_SpecialCharacters(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/special.txt",
		Content:  "interface{}\nmap[string]int\nfunc() error\n$variable\n*pointer",
	})

	t.Run("match curly braces", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "interface\\{\\}",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 1 {
			t.Errorf("Expected 1 match, got %d", len(matches))
		}
	})

	t.Run("match square brackets", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "map\\[.*\\]",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 1 {
			t.Errorf("Expected 1 match, got %d", len(matches))
		}
	})

	t.Run("match parentheses", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "func\\(\\)",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 1 {
			t.Errorf("Expected 1 match, got %d", len(matches))
		}
	})

	t.Run("match dollar sign", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "\\$variable",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 1 {
			t.Errorf("Expected 1 match, got %d", len(matches))
		}
	})

	t.Run("match asterisk", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "\\*pointer",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 1 {
			t.Errorf("Expected 1 match, got %d", len(matches))
		}
	})
}

func TestInMemoryBackend_GrepRaw_Concurrent(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	for i := 0; i < 10; i++ {
		backend.Write(ctx, &WriteRequest{
			FilePath: fmt.Sprintf("/file%d.txt", i),
			Content:  fmt.Sprintf("content%d with error message", i),
		})
	}

	t.Run("concurrent grep operations", func(t *testing.T) {
		done := make(chan bool)
		for i := 0; i < 10; i++ {
			go func() {
				_, err := backend.GrepRaw(ctx, &GrepRequest{
					Pattern: "error",
				})
				if err != nil {
					t.Errorf("Concurrent GrepRaw failed: %v", err)
				}
				done <- true
			}()
		}

		for i := 0; i < 10; i++ {
			<-done
		}
	})

	t.Run("parallel file processing", func(t *testing.T) {
		backend := NewInMemoryBackend()
		for i := 0; i < 100; i++ {
			backend.Write(ctx, &WriteRequest{
				FilePath: fmt.Sprintf("/large/file%d.go", i),
				Content:  fmt.Sprintf("package main\nimport \"log\"\nfunc test%d() {\n\tlog.Error(\"error %d\")\n}", i, i),
			})
		}

		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:  "log\\.Error",
			FileType: "go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 100 {
			t.Errorf("Expected 100 matches, got %d", len(matches))
		}
	})

	t.Run("single file no parallelism", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{
			FilePath: "/single.txt",
			Content:  "error line 1\nerror line 2\nerror line 3",
		})

		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "error",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 3 {
			t.Errorf("Expected 3 matches, got %d", len(matches))
		}
	})

	t.Run("empty files list", func(t *testing.T) {
		backend := NewInMemoryBackend()
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "anything",
			Path:    "/nonexistent",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) != 0 {
			t.Errorf("Expected 0 matches, got %d", len(matches))
		}
	})

	t.Run("concurrent operations are safe", func(t *testing.T) {
		backend := NewInMemoryBackend()
		for i := 0; i < 20; i++ {
			backend.Write(ctx, &WriteRequest{
				FilePath: fmt.Sprintf("/concurrent/file%d.txt", i),
				Content:  fmt.Sprintf("line1\nline2\npattern%d\nline4", i),
			})
		}

		done := make(chan error, 5)
		for i := 0; i < 5; i++ {
			go func(id int) {
				_, err := backend.GrepRaw(ctx, &GrepRequest{
					Pattern: "pattern\\d+",
				})
				done <- err
			}(i)
		}

		for i := 0; i < 5; i++ {
			if err := <-done; err != nil {
				t.Errorf("Concurrent operation %d failed: %v", i, err)
			}
		}
	})
}

func BenchmarkInMemoryBackend_GrepRaw(b *testing.B) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	for i := 0; i < 100; i++ {
		content := fmt.Sprintf(`package main

import (
	"fmt"
	"log"
)

func process%d() error {
	log.Error("processing error %d")
	fmt.Println("hello world")
	return nil
}

func calculate%d(x, y int) int {
	return x + y
}
`, i, i, i)
		backend.Write(ctx, &WriteRequest{
			FilePath: fmt.Sprintf("/project/src/file%d.go", i),
			Content:  content,
		})
	}

	b.Run("parallel_grep", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, err := backend.GrepRaw(ctx, &GrepRequest{
				Pattern:  "log\\.Error",
				FileType: "go",
			})
			if err != nil {
				b.Fatalf("GrepRaw failed: %v", err)
			}
		}
	})

	b.Run("with_glob_filter", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, err := backend.GrepRaw(ctx, &GrepRequest{
				Pattern: "Error",
				Glob:    "**/*.go",
			})
			if err != nil {
				b.Fatalf("GrepRaw failed: %v", err)
			}
		}
	})

	b.Run("case_insensitive", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, err := backend.GrepRaw(ctx, &GrepRequest{
				Pattern:         "ERROR",
				CaseInsensitive: true,
			})
			if err != nil {
				b.Fatalf("GrepRaw failed: %v", err)
			}
		}
	})
}

func TestInMemoryBackend_GrepRaw_ComplexScenarios(t *testing.T) {
	backend := NewInMemoryBackend()
	ctx := context.Background()

	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/src/main.go",
		Content:  "package main\nimport \"log\"\nfunc main() {\n\tlog.Error(\"error\")\n}",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/src/utils/helper.go",
		Content:  "package utils\nfunc Helper() error {\n\treturn nil\n}",
	})
	backend.Write(ctx, &WriteRequest{
		FilePath: "/project/test/main_test.go",
		Content:  "package main\nimport \"testing\"\nfunc TestMain(t *testing.T) {\n}",
	})

	t.Run("combine path and file type filters", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:  "package",
			Path:     "/project/src",
			FileType: "go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasPrefix(match.Path, "/project/src") {
				t.Errorf("Expected path to start with /project/src, got: %s", match.Path)
			}
			if !strings.HasSuffix(match.Path, ".go") {
				t.Errorf("Expected .go file, got: %s", match.Path)
			}
		}
	})

	t.Run("complex regex with case insensitive", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern:         "func\\s+\\w+",
			CaseInsensitive: true,
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		if len(matches) == 0 {
			t.Error("Expected at least 1 match for function declarations")
		}
	})

	t.Run("glob with directory structure", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "package",
			Glob:    "*_test.go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasSuffix(match.Path, "_test.go") {
				t.Errorf("Expected test file, got: %s", match.Path)
			}
		}
	})

	t.Run("glob with recursive pattern", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "package",
			Glob:    "**/*.go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasSuffix(match.Path, ".go") {
				t.Errorf("Expected .go file, got: %s", match.Path)
			}
		}
		if len(matches) == 0 {
			t.Error("Expected at least 1 match for **/*.go pattern")
		}
	})

	t.Run("glob with path prefix", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "package",
			Glob:    "src/**/*.go",
			Path:    "/project",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if !strings.HasPrefix(match.Path, "/project/src") {
				t.Errorf("Expected path to start with /project/src, got: %s", match.Path)
			}
			if !strings.HasSuffix(match.Path, ".go") {
				t.Errorf("Expected .go file, got: %s", match.Path)
			}
		}
	})

	t.Run("glob simple filename pattern", func(t *testing.T) {
		matches, err := backend.GrepRaw(ctx, &GrepRequest{
			Pattern: "package",
			Glob:    "main.go",
		})
		if err != nil {
			t.Fatalf("GrepRaw failed: %v", err)
		}
		for _, match := range matches {
			if filepath.Base(match.Path) != "main.go" {
				t.Errorf("Expected filename 'main.go', got: %s", match.Path)
			}
		}
	})
}

func TestInMemoryBackend_Read_Scenarios(t *testing.T) {
	ctx := context.Background()

	t.Run("empty file returns empty content", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/empty.txt", Content: ""})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/empty.txt"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "" {
			t.Errorf("expected empty content, got %q", content.Content)
		}
	})

	t.Run("single-line file without newline", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/single.txt", Content: "hello"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/single.txt"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "hello" {
			t.Errorf("expected %q, got %q", "hello", content.Content)
		}
	})

	t.Run("offset 0 and offset 1 both start from first line", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"})

		c0, _ := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 0, Limit: 1})
		c1, _ := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 1, Limit: 1})
		if c0.Content != c1.Content {
			t.Errorf("Offset=0 (%q) and Offset=1 (%q) should return the same first line", c0.Content, c1.Content)
		}
		if c0.Content != "a" {
			t.Errorf("expected first line %q, got %q", "a", c0.Content)
		}
	})

	t.Run("file with trailing newline preserves trailing empty line", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/trail.txt", Content: "line1\nline2\n"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/trail.txt"})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "line1\nline2\n" {
			t.Errorf("expected %q, got %q", "line1\nline2\n", content.Content)
		}
		lines := strings.Split(content.Content, "\n")
		if len(lines) != 3 { // ["line1", "line2", ""]
			t.Errorf("expected 3 elements from split, got %d", len(lines))
		}
	})

	t.Run("offset exactly at last line", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"})

		// Offset=3 (1-based) → last line "c"
		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 3, Limit: 10})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "c" {
			t.Errorf("expected %q, got %q", "c", content.Content)
		}
	})

	t.Run("offset one beyond last line returns empty", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 4, Limit: 10})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "" {
			t.Errorf("expected empty content, got %q", content.Content)
		}
	})

	t.Run("limit=1 reads exactly one line", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc"})

		for i, expected := range []string{"a", "b", "c"} {
			content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: i + 1, Limit: 1})
			if err != nil {
				t.Fatalf("line %d: unexpected error: %v", i+1, err)
			}
			if content.Content != expected {
				t.Errorf("line %d: expected %q, got %q", i+1, expected, content.Content)
			}
		}
	})

	t.Run("sliding window reads consecutive ranges correctly", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "l1\nl2\nl3\nl4\nl5"})

		tests := []struct {
			offset   int
			limit    int
			expected string
		}{
			{1, 2, "l1\nl2"},
			{2, 2, "l2\nl3"},
			{3, 2, "l3\nl4"},
			{4, 2, "l4\nl5"},
			{5, 2, "l5"},
		}
		for _, tt := range tests {
			content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: tt.offset, Limit: tt.limit})
			if err != nil {
				t.Fatalf("offset=%d limit=%d: unexpected error: %v", tt.offset, tt.limit, err)
			}
			if content.Content != tt.expected {
				t.Errorf("offset=%d limit=%d: expected %q, got %q", tt.offset, tt.limit, tt.expected, content.Content)
			}
		}
	})

	t.Run("file with only newlines", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/newlines.txt", Content: "\n\n\n"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/newlines.txt", Offset: 2, Limit: 1})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		// Line 2 is an empty string between two newlines
		if content.Content != "" {
			t.Errorf("expected empty line content, got %q", content.Content)
		}
	})
}

func TestInMemoryBackend_Read_NoLimit(t *testing.T) {
	ctx := context.Background()

	t.Run("limit=0 reads all lines", func(t *testing.T) {
		backend := NewInMemoryBackend()
		fullContent := "line1\nline2\nline3\nline4\nline5"
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: fullContent})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Limit: 0})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != fullContent {
			t.Errorf("expected %q, got %q", fullContent, content.Content)
		}
	})

	t.Run("negative limit reads all lines", func(t *testing.T) {
		backend := NewInMemoryBackend()
		fullContent := "a\nb\nc"
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: fullContent})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Limit: -1})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != fullContent {
			t.Errorf("expected %q, got %q", fullContent, content.Content)
		}
	})

	t.Run("limit=0 with offset reads from offset to end", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb\nc\nd\ne"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 3, Limit: 0})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "c\nd\ne" {
			t.Errorf("expected %q, got %q", "c\nd\ne", content.Content)
		}
	})

	t.Run("limit=0 with offset beyond content returns empty", func(t *testing.T) {
		backend := NewInMemoryBackend()
		backend.Write(ctx, &WriteRequest{FilePath: "/f.txt", Content: "a\nb"})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/f.txt", Offset: 10, Limit: 0})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != "" {
			t.Errorf("expected empty content, got %q", content.Content)
		}
	})

	t.Run("limit=0 reads file with more than 2000 lines", func(t *testing.T) {
		backend := NewInMemoryBackend()
		var b strings.Builder
		totalLines := 2500
		for i := 1; i <= totalLines; i++ {
			if i > 1 {
				b.WriteString("\n")
			}
			b.WriteString("line" + strconv.Itoa(i))
		}
		fullContent := b.String()
		backend.Write(ctx, &WriteRequest{FilePath: "/big.txt", Content: fullContent})

		content, err := backend.Read(ctx, &ReadRequest{FilePath: "/big.txt", Limit: 0})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if content.Content != fullContent {
			t.Errorf("expected all %d lines, got %d lines",
				totalLines, strings.Count(content.Content, "\n")+1)
		}
	})
}


import { CliError } from '@jackwener/opencli/errors';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { postHupuJson } from './utils.js';
cli({
    site: 'hupu',
    name: 'unlike',
    access: 'write',
    description: '取消点赞虎扑回复 (需要登录)',
    domain: 'bbs.hupu.com',
    strategy: Strategy.COOKIE, // 需要Cookie认证
    navigateBefore: false,
    args: [
        {
            name: 'tid',
            required: true,
            positional: true,
            help: '帖子ID（9位数字）'
        },
        {
            name: 'pid',
            required: true,
            positional: true,
            help: '回复ID'
        },
        {
            name: 'fid',
            required: true,
            help: '板块ID（如278汽车区）'
        }
    ],
    columns: ['status', 'message'],
    func: async (page, kwargs) => {
        const { tid, pid, fid } = kwargs;
        const url = 'https://bbs.hupu.com/pcmapi/pc/bbs/v1/reply/cancelLight';
        // 构建请求体（与点赞相同）
        const body = {
            tid,
            pid,
            puid: '',
            fid,
            shumei_id: '',
            deviceid: ''
        };
        try {
            const result = await postHupuJson(page, tid, url, body, 'Unlike Hupu reply');
            // 处理响应
            if (result.code === 1) {
                return [{
                        status: '✅ 取消点赞成功',
                        message: ''
                    }];
            }
            else if (result.code === 0 && result.msg === '你还没有点亮过这个回帖') {
                return [{
                        status: '⚠ 你还没点赞过',
                        message: result.msg || ''
                    }];
            }
            else if (result.code === 0) {
                return [{
                        status: '⚠ 操作未执行',
                        message: result.msg || result.message || ''
                    }];
            }
            else {
                throw new Error(`接口错误 code=${result.code}: ${result.msg || result.message}`);
            }
        }
        catch (error) {
            if (error instanceof CliError)
                throw error;
            const errorMessage = error instanceof Error ? error.message : String(error);
            throw new Error(`取消点赞失败: ${errorMessage}`);
        }
    },
});


/*
 * Copyright 2025 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.netflix.maestro.timetrigger.utils;

import com.netflix.maestro.engine.utils.TriggerSubscriptionClient;
import com.netflix.maestro.metrics.MaestroMetrics;
import com.netflix.maestro.models.definition.Workflow;
import com.netflix.maestro.models.trigger.TriggerUuids;
import com.netflix.maestro.timetrigger.Constants;
import com.netflix.maestro.timetrigger.metrics.MetricConstants;
import com.netflix.maestro.timetrigger.models.TimeTriggerExecution;
import com.netflix.maestro.timetrigger.models.TimeTriggerWithWatermark;
import com.netflix.maestro.timetrigger.producer.TimeTriggerProducer;
import java.sql.Connection;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/** Time trigger subscription client. */
@Slf4j
@AllArgsConstructor
public class TimeTriggerSubscriptionClient implements TriggerSubscriptionClient {
  private final TimeTriggerProducer triggerProducer;
  private final MaestroMetrics metrics;

  @Override
  public void upsertTriggerSubscription(
      Connection conn, Workflow workflow, TriggerUuids current, TriggerUuids previous) {
    if (workflow.getTimeTriggers() != null
        && !workflow.getTimeTriggers().isEmpty()
        && current != null
        && current.getTimeTriggerUuid() != null) {
      if (previous == null
          || !Objects.equals(current.getTimeTriggerUuid(), previous.getTimeTriggerUuid())) {
        LOG.info(
            "Update time trigger [{}] for workflow id [{}]",
            current.getTimeTriggerUuid(),
            workflow.getId());
        TimeTriggerExecution execution = convertToExecution(workflow, current);
        insertSubscription(execution);
      } else {
        LOG.info(
            "No time trigger update for workflow id [{}] as it has been sent.", workflow.getId());
      }
    }
  }

  private void insertSubscription(TimeTriggerExecution execution) {
    // Create initial execution message
    triggerProducer.push(execution, Constants.MESSAGE_DELAY_FIRST_EXECUTION);
    metrics.counter(MetricConstants.CREATE_SUBSCRIPTION_METRIC, getClass());
  }

  /** Convert subscription to execution message. */
  private TimeTriggerExecution convertToExecution(Workflow workflow, TriggerUuids current) {
    var timeTriggerWithWatermarks =
        workflow.getTimeTriggers().stream()
            .map(
                t ->
                    TimeTriggerWithWatermark.builder()
                        .lastTriggerTimestamp(System.currentTimeMillis())
                        .timeTrigger(t)
                        .build())
            .toList();
    return TimeTriggerExecution.builder()
        .workflowId(workflow.getId())
        .workflowVersion(Constants.TIME_TRIGGER_WORKFLOW_VERSION)
        .workflowTriggerUuid(current.getTimeTriggerUuid())
        .timeTriggersWithWatermarks(timeTriggerWithWatermarks)
        .build();
  }
}


import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "#/lib/utils.ts"

const buttonVariants = cva(
  "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default:
          "h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
        xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
        sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
        lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
        icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
        "icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
        "icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
        "icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Button({
  className,
  variant = "default",
  size = "default",
  ...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
  return (
    <ButtonPrimitive
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }