phase: 121_git_source_tokens
**Phase 121 final verification pass — all green** (all 4 tasks already in `complete/`; verified, no defects found, no changes needed) - Verified implementation vs phase design: migration `0021` (reversible, round-tripped via `alembic downgrade base` + `upgrade head` → head `0021`), `GitSource.token` column, `normalize_credential`/`clone_url_for`/`sanitize_url`, clone callers switched (`sync.py`, `import_docs.py`), masked token fields in add form + editor, `extra="forbid"` output shapes - Tests: `uv run pytest` → 2662 passed, 0 failed (exit 0); `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (≥90% gate) - Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings - E2E in isolation: `uv run pytest tests/e2e/test_git_source_tokens.py -v --no-cov` → **4 passed** Completion criteria: 1. Private repo (UI add or pasted embedded-token URL) clones with injected token; token absent from every API response, page text, title attr, and full HTML — **PASS** (integration raw-JSON assertions + E2E `_assert_token_nowhere`) 2. Legacy embedded-token rows still clone from stored URL; output sanitized — **PASS** (`test_sync_legacy_row_clones_with_original_stored_url`, `test_get_masks_legacy_embedded_token_row`, env-fallback masking) 3. Public/local sources byte-identical — **PASS** (verbatim-URL + no-userinfo-unchanged tests) 4. pytest / coverage / ruff / pyright — **PASS** (see above) 5. Commit + phase move — harness responsibility; task files already in `complete/`, changes left in working tree (no commit made, per protocol) Notable: no deviations; DB left at head, functional. Next pending phase: **122_image_documents** (then 123_chat_image_questions).
This commit is contained in:
+46
-2
@@ -583,6 +583,14 @@ class GitSourceIn(BaseModel):
|
||||
|
||||
``include_hidden`` (phase 105) is optional at create time (absent →
|
||||
stored ``False`` — A4).
|
||||
|
||||
``token`` (phase 121, LOCKED A2) is the masked private-repo
|
||||
credential from the Sources page: optional at create time (absent/
|
||||
None = no credential — public repo), trimmed *before* the length
|
||||
constraints run (the ``_trim_url`` precedent), max 500. It is a
|
||||
WRITE-ONLY field — stored in the dedicated ``git_sources.token``
|
||||
column and NEVER echoed back by any output shape (``GitSourceOut``
|
||||
/ ``GitSourceRow`` carry no token field by contract).
|
||||
"""
|
||||
|
||||
kind: Literal["git", "local"] = "git"
|
||||
@@ -590,6 +598,7 @@ class GitSourceIn(BaseModel):
|
||||
path: str | None = Field(default=None, min_length=1, max_length=2000)
|
||||
ignore_paths: list[str] | None = Field(default=None)
|
||||
include_hidden: bool | None = Field(default=None)
|
||||
token: str | None = Field(default=None, max_length=500)
|
||||
|
||||
@field_validator("url", mode="before")
|
||||
@classmethod
|
||||
@@ -601,6 +610,11 @@ class GitSourceIn(BaseModel):
|
||||
def _trim_path(cls, v: object) -> object:
|
||||
return v.strip() if isinstance(v, str) else v
|
||||
|
||||
@field_validator("token", mode="before")
|
||||
@classmethod
|
||||
def _trim_token(cls, v: object) -> object:
|
||||
return v.strip() if isinstance(v, str) else v
|
||||
|
||||
|
||||
class GitSourceOut(BaseModel):
|
||||
"""One created git source as returned by ``POST`` (phase 35, task 02).
|
||||
@@ -614,8 +628,18 @@ class GitSourceOut(BaseModel):
|
||||
list — non-null (a row created without it reports ``[]``).
|
||||
``include_hidden`` (phase 105) is the stored flag — a row created
|
||||
without it reports ``False`` (A4).
|
||||
|
||||
There is deliberately NO ``token`` field (phase 121, LOCKED A2):
|
||||
the private-repo credential is stored in the dedicated
|
||||
``git_sources.token`` column and is NEVER a response field — it
|
||||
never reaches the UI or any API output. ``extra="forbid"`` makes
|
||||
the omission a structural contract, not an accident: constructing
|
||||
this model with a ``token`` key raises, so a regression that tries
|
||||
to echo the credential back cannot even build the shape.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: uuid.UUID | None
|
||||
url: str
|
||||
added_at: datetime | None
|
||||
@@ -637,8 +661,16 @@ class GitSourceRow(BaseModel):
|
||||
store a list on) report ``[]``. ``include_hidden`` (phase 105) is
|
||||
the row's stored flag — env-fallback rows (no DB row to store a flag
|
||||
on) report ``False`` (the ``ignore_paths: []`` precedent).
|
||||
|
||||
There is deliberately NO ``token`` field (phase 121, LOCKED A2):
|
||||
same contract as :class:`GitSourceOut` — the credential never
|
||||
reaches the UI or any API output, and ``extra="forbid"`` makes the
|
||||
omission structural (constructing a row with a ``token`` key
|
||||
raises).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: uuid.UUID | None
|
||||
kind: Literal["git", "local"]
|
||||
url: str
|
||||
@@ -658,12 +690,24 @@ class GitSourcePatchIn(BaseModel):
|
||||
normalized + A4-validated, becomes the row's whole list — empty
|
||||
list clears all; every pre-phase-105 client always sends the
|
||||
list, so their behavior is byte-identical). ``include_hidden``
|
||||
(phase 105) when present sets the stored flag. Both absent →
|
||||
200 no-op (the row is untouched).
|
||||
(phase 105) when present sets the stored flag. ``token`` (phase
|
||||
121, LOCKED A2) is TRI-STATE — the three-way semantics the masked
|
||||
edit field depends on: **absent/None = no change** (keep the row's
|
||||
stored credential), **non-empty = replace**, **empty string =
|
||||
clear** (the UI offers replace; clear exists for API completeness).
|
||||
Trimmed *before* the length constraints run (the ``GitSourceIn``
|
||||
``_trim_token`` precedent — whitespace-only counts as a clear),
|
||||
max 500. All absent → 200 no-op (the row is untouched).
|
||||
"""
|
||||
|
||||
ignore_paths: list[str] | None = Field(default=None)
|
||||
include_hidden: bool | None = Field(default=None)
|
||||
token: str | None = Field(default=None, max_length=500)
|
||||
|
||||
@field_validator("token", mode="before")
|
||||
@classmethod
|
||||
def _trim_token(cls, v: object) -> object:
|
||||
return v.strip() if isinstance(v, str) else v
|
||||
|
||||
|
||||
class GitSourceList(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user