Initial Aurora Core 0.1 implementation
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

Production-grade media metadata backend per docs/aurora-core-spec.md:
Aurora-owned UUIDv7 identity system, normalized movie/TV/people/credits
schema, provider abstraction with a TMDb adapter, an idempotent async
ingestion pipeline (normalize -> match -> upsert -> provenance), a
Postgres FTS/trigram search layer, a stateless FastAPI v1 API with
Redis caching/rate-limiting, arq workers, dev/prod Docker Compose
topologies, unit + integration + load tests, CI, and architecture/
database/API/ERD documentation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-04 17:34:03 -05:00
co-authored by Claude Sonnet 5
commit c20df1c503
111 changed files with 10041 additions and 0 deletions
View File
+87
View File
@@ -0,0 +1,87 @@
"""Shared pytest fixtures.
Integration/API tests need a real PostgreSQL instance (JSONB, trigram/FTS
indexes, and native enums are Postgres-specific — see docs/database.md) and
Redis. Point `TEST_DATABASE_URL` / `TEST_REDIS_URL` at throwaway instances,
e.g. via `docker compose up -d postgres redis` and a separate test database:
createdb -h localhost -U aurora aurora_core_test
Unit tests (tests/unit/) never touch either and always run offline.
"""
from __future__ import annotations
import os
from collections.abc import AsyncGenerator
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
TEST_DATABASE_URL = os.environ.get(
"TEST_DATABASE_URL", "postgresql+asyncpg://aurora:aurora@localhost:5432/aurora_core_test"
)
TEST_REDIS_URL = os.environ.get("TEST_REDIS_URL", "redis://localhost:6379/15")
os.environ.setdefault("DATABASE_URL", TEST_DATABASE_URL)
os.environ.setdefault("REDIS_URL", TEST_REDIS_URL)
os.environ.setdefault("AURORA_ENV", "test")
os.environ.setdefault("AURORA_SECRET_KEY", "test-secret")
os.environ.setdefault("AURORA_ADMIN_API_KEYS", "test-admin-key")
def _requires_postgres() -> bool:
return "--no-db" not in os.environ.get("PYTEST_ADDOPTS", "")
@pytest_asyncio.fixture(scope="session")
async def engine():
from sqlalchemy import text
from app.models import Base
eng = create_async_engine(TEST_DATABASE_URL)
try:
async with eng.begin() as conn:
# Tests build schema straight from ORM metadata for speed and
# to catch model/migration drift; docs/database.md notes this
# is deliberately *not* how the running application migrates.
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm"))
await conn.run_sync(Base.metadata.create_all)
except Exception as exc: # pragma: no cover - environment guard
pytest.skip(f"PostgreSQL not reachable at {TEST_DATABASE_URL}: {exc}")
yield eng
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await eng.dispose()
@pytest_asyncio.fixture
async def db_session(engine) -> AsyncGenerator[AsyncSession, None]:
connection = await engine.connect()
transaction = await connection.begin()
session_factory = async_sessionmaker(bind=connection, expire_on_commit=False)
session = session_factory()
yield session
await session.close()
await transaction.rollback()
await connection.close()
@pytest_asyncio.fixture
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
from app.core.database import get_db
from app.main import app
async def _override_get_db():
yield db_session
app.dependency_overrides[get_db] = _override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
yield ac
app.dependency_overrides.clear()
View File
+81
View File
@@ -0,0 +1,81 @@
"""Minimal data builders for integration tests — inserts ORM rows directly
so tests don't depend on network calls to a real provider."""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.ids import aurora_id
from app.models.artwork import Artwork
from app.models.credit import Credit
from app.models.enums import ArtworkType, MovieStatus
from app.models.external_id import ExternalID
from app.models.genre import Genre, MovieGenre
from app.models.movie import Movie
from app.models.person import Person
from app.models.provider import Provider
async def make_provider(session: AsyncSession, slug: str = "tmdb") -> Provider:
provider = Provider(slug=slug, name=slug.upper())
session.add(provider)
await session.flush()
return provider
async def make_movie(
session: AsyncSession, *, title: str = "The Matrix", provider: Provider | None = None
) -> Movie:
movie = Movie(
title=title,
sort_title=title,
overview="A hacker discovers reality is a simulation.",
status=MovieStatus.RELEASED,
runtime_minutes=136,
)
session.add(movie)
await session.flush()
genre = Genre(name="Science Fiction", slug="science-fiction")
session.add(genre)
await session.flush()
session.add(MovieGenre(movie_id=movie.id, genre_id=genre.id))
person = Person(name="Keanu Reeves")
session.add(person)
await session.flush()
session.add(
Credit(
person_id=person.id,
entity_type="movie",
entity_id=movie.id,
department="Acting",
character="Neo",
billing_order=0,
)
)
session.add(
Artwork(
entity_type="movie",
entity_id=movie.id,
artwork_type=ArtworkType.POSTER,
provider_id=(provider or await make_provider(session)).id,
original_url="https://image.tmdb.org/t/p/original/poster.jpg",
is_primary=True,
)
)
if provider is not None:
session.add(
ExternalID(
entity_type="movie", entity_id=movie.id, provider_id=provider.id, external_id="603"
)
)
await session.flush()
return movie
def movie_aurora_id(movie: Movie) -> str:
return aurora_id("movie", str(movie.id))
+26
View File
@@ -0,0 +1,26 @@
"""Integration tests for liveness/readiness (section 24). Requires
PostgreSQL and Redis — see tests/conftest.py."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.usefixtures("db_session")
@pytest.mark.asyncio
async def test_health_is_always_ok(client: AsyncClient):
response = await client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.asyncio
async def test_ready_reports_postgresql_dependency(client: AsyncClient):
response = await client.get("/ready")
assert response.status_code == 200
body = response.json()
names = {dep["name"] for dep in body["dependencies"]}
assert "postgresql" in names
assert "redis" in names
+105
View File
@@ -0,0 +1,105 @@
"""Integration tests for the movie ingestion pipeline (section 15): a fake
provider stands in for TMDb so these run without network access, while
still exercising the real normalize -> match -> upsert -> provenance path
against PostgreSQL."""
from __future__ import annotations
from typing import Any
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.credit import Credit
from app.models.genre import Genre
from app.models.movie import Movie
from app.models.provider import ProviderRecord
from app.providers.base import MetadataProvider
from app.services.ingestion.movie_ingestion import ingest_movie
from tests.unit.test_normalizer import FAKE_MOVIE
class FakeTMDbProvider(MetadataProvider):
slug = "tmdb"
name = "The Movie Database (TMDb)"
def __init__(self, payload: dict[str, Any]) -> None:
self._payload = payload
async def get_movie(self, external_id: str) -> dict[str, Any]:
return self._payload
async def get_series(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]:
raise NotImplementedError
async def get_person(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def search_movie(self, query: str) -> list[dict[str, Any]]:
raise NotImplementedError
@pytest.mark.asyncio
async def test_ingest_movie_creates_normalized_record(db_session: AsyncSession):
provider = FakeTMDbProvider(FAKE_MOVIE)
movie_id = await ingest_movie(db_session, provider, "603")
await db_session.commit()
movie = await db_session.get(Movie, movie_id)
assert movie is not None
assert movie.title == "The Matrix"
assert movie.runtime_minutes == 136
genres = (await db_session.execute(select(Genre.name))).scalars().all()
assert "Action" in genres
credits_ = (
(await db_session.execute(select(Credit).where(Credit.entity_id == movie_id)))
.scalars()
.all()
)
assert any(c.character == "Neo" for c in credits_)
record = (
await db_session.execute(
select(ProviderRecord).where(ProviderRecord.external_id == "603")
)
).scalar_one()
assert record.aurora_entity_id == movie_id
assert record.match_confidence.value == "new"
@pytest.mark.asyncio
async def test_ingest_movie_is_idempotent(db_session: AsyncSession):
"""Re-running ingestion for the same provider + external_id must update
the same Aurora movie, never create a duplicate (section 2: zero
duplicate ingestion records)."""
provider = FakeTMDbProvider(FAKE_MOVIE)
first_id = await ingest_movie(db_session, provider, "603")
await db_session.commit()
updated_payload = {**FAKE_MOVIE, "title": "The Matrix (Updated Title)"}
second_provider = FakeTMDbProvider(updated_payload)
second_id = await ingest_movie(db_session, second_provider, "603")
await db_session.commit()
assert first_id == second_id
all_movies = (await db_session.execute(select(Movie))).scalars().all()
assert len(all_movies) == 1
assert all_movies[0].title == "The Matrix (Updated Title)"
credits_ = (
(await db_session.execute(select(Credit).where(Credit.entity_id == first_id)))
.scalars()
.all()
)
# Credits are replaced wholesale on re-sync, not duplicated.
assert len(credits_) == 2 # 1 cast + 1 crew from FAKE_MOVIE
+91
View File
@@ -0,0 +1,91 @@
"""Integration tests for the Movie API (section 17/18). Requires
PostgreSQL and Redis — see tests/conftest.py."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from tests.integration.factories import make_movie, make_provider, movie_aurora_id
@pytest.mark.asyncio
async def test_get_movie_returns_normalized_record(client: AsyncClient, db_session: AsyncSession):
provider = await make_provider(db_session)
movie = await make_movie(db_session, provider=provider)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}")
assert response.status_code == 200
body = response.json()
assert body["id"] == movie_aurora_id(movie)
assert body["type"] == "movie"
assert body["title"] == "The Matrix"
assert body["runtime"] == 136
assert {g["name"] for g in body["genres"]} == {"Science Fiction"}
assert len(body["cast"]) == 1
assert body["cast"][0]["character"] == "Neo"
assert body["images"]["posters"][0]["is_primary"] is True
assert body["external_ids"]["tmdb"] == "603"
@pytest.mark.asyncio
async def test_get_movie_404_for_unknown_id(client: AsyncClient):
from app.core.ids import aurora_id, new_uuid7
unknown_id = aurora_id("movie", new_uuid7())
response = await client.get(f"/api/v1/movies/{unknown_id}")
assert response.status_code == 404
assert response.json()["error"]["code"] == "movie_not_found"
@pytest.mark.asyncio
async def test_get_movie_rejects_malformed_id(client: AsyncClient):
response = await client.get("/api/v1/movies/not-a-real-id")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_get_movie_rejects_wrong_entity_type_id(
client: AsyncClient, db_session: AsyncSession
):
# A well-formed person ID must not resolve as a movie.
from app.core.ids import aurora_id, new_uuid7
person_shaped_id = aurora_id("person", new_uuid7())
response = await client.get(f"/api/v1/movies/{person_shaped_id}")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_get_movie_credits(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/credits")
assert response.status_code == 200
body = response.json()
assert len(body["cast"]) == 1
assert body["cast"][0]["person"]["name"] == "Keanu Reeves"
@pytest.mark.asyncio
async def test_get_movie_images(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/images")
assert response.status_code == 200
assert len(response.json()["images"]["posters"]) == 1
@pytest.mark.asyncio
async def test_get_movie_videos_empty(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session)
await db_session.commit()
response = await client.get(f"/api/v1/movies/{movie_aurora_id(movie)}/videos")
assert response.status_code == 200
assert response.json()["videos"] == []
+103
View File
@@ -0,0 +1,103 @@
"""Load test for Aurora Core 0.1 (section 30/2).
Simulates the read-heavy interactive traffic Core 0.1 targets: searching,
opening movie/series/person detail pages, and paging through search
results. Run against a seeded environment (ingest a reasonable movie/series/
person catalog first — an empty database will pass load tests trivially but
tell you nothing about real query cost).
Usage:
pip install locust
locust -f tests/load/locustfile.py --host http://localhost:8000
Then open http://localhost:8089 and start a run with up to 500 users to
verify the section 2 targets (search p95 < 500ms, other endpoints p95 <
250ms, 5xx rate < 0.1%). Locust reports p50/p95/p99 latency and failure
rate directly; compare them against docs/architecture.md's targets.
See docs/architecture.md for how to seed sample data before running this.
"""
from __future__ import annotations
import random
from locust import HttpUser, between, task
SEARCH_TERMS = [
"matrix",
"star wars",
"alien",
"the office",
"breaking bad",
"keanu reeves",
"christopher nolan",
"marvel",
"batman",
"inception",
]
class AuroraCoreUser(HttpUser):
"""A typical interactive client: search, then drill into a result."""
wait_time = between(1, 3)
def on_start(self) -> None:
self._known_movie_ids: list[str] = []
self._known_series_ids: list[str] = []
self._known_person_ids: list[str] = []
@task(5)
def search(self) -> None:
query = random.choice(SEARCH_TERMS)
limit = random.choice([5, 10, 20])
with self.client.get(
f"/api/v1/search?q={query}&limit={limit}",
name="/api/v1/search",
catch_response=True,
) as response:
if response.status_code != 200:
response.failure(f"search failed: {response.status_code}")
return
body = response.json()
self._known_movie_ids.extend(m["id"] for m in body.get("movies", []))
self._known_series_ids.extend(s["id"] for s in body.get("series", []))
self._known_person_ids.extend(p["id"] for p in body.get("people", []))
# Keep the pools bounded so memory doesn't grow unbounded over a
# long run.
self._known_movie_ids = self._known_movie_ids[-200:]
self._known_series_ids = self._known_series_ids[-200:]
self._known_person_ids = self._known_person_ids[-200:]
@task(4)
def open_movie(self) -> None:
if not self._known_movie_ids:
return
movie_id = random.choice(self._known_movie_ids)
self.client.get(f"/api/v1/movies/{movie_id}", name="/api/v1/movies/[id]")
self.client.get(
f"/api/v1/movies/{movie_id}/credits", name="/api/v1/movies/[id]/credits"
)
@task(2)
def open_series(self) -> None:
if not self._known_series_ids:
return
series_id = random.choice(self._known_series_ids)
self.client.get(f"/api/v1/series/{series_id}", name="/api/v1/series/[id]")
self.client.get(
f"/api/v1/series/{series_id}/seasons", name="/api/v1/series/[id]/seasons"
)
@task(2)
def open_person(self) -> None:
if not self._known_person_ids:
return
person_id = random.choice(self._known_person_ids)
self.client.get(f"/api/v1/people/{person_id}", name="/api/v1/people/[id]")
@task(1)
def health_probe(self) -> None:
self.client.get("/health", name="/health")
View File
+73
View File
@@ -0,0 +1,73 @@
"""Unit tests for the Aurora ID system (app/core/ids.py) — section 3."""
from __future__ import annotations
import pytest
from app.core.ids import (
ENTITY_PREFIXES,
aurora_id,
is_valid_aurora_id,
new_uuid7,
parse_aurora_id,
)
def test_new_uuid7_is_unique_and_well_formed():
ids = {new_uuid7() for _ in range(1000)}
assert len(ids) == 1000
for value in list(ids)[:5]:
assert len(value) == 36 # canonical hyphenated form
def test_aurora_id_roundtrips_through_parse():
u = new_uuid7()
aid = aurora_id("movie", u)
assert aid.startswith("aur_movie_")
assert parse_aurora_id(aid, expected_type="movie") == u
def test_aurora_id_never_leaks_provider_identity():
# Aurora IDs must not be derived from or embed provider IDs (section 3).
u = new_uuid7()
aid = aurora_id("movie", u)
assert "550" not in aid # a plausible TMDb id, just shouldn't appear
assert "tmdb" not in aid
def test_every_entity_prefix_produces_a_parseable_id():
for entity_type in ENTITY_PREFIXES:
aid = aurora_id(entity_type, new_uuid7())
assert is_valid_aurora_id(aid, expected_type=entity_type)
def test_parse_rejects_wrong_entity_type():
aid = aurora_id("movie", new_uuid7())
with pytest.raises(ValueError):
parse_aurora_id(aid, expected_type="person")
assert is_valid_aurora_id(aid, expected_type="person") is False
@pytest.mark.parametrize(
"malformed",
[
"",
"not-an-aurora-id",
"aur_movie_",
"aur_movie_tooshort",
"aur_unknownprefix_" + "0" * 26,
"aur_movie_" + "!" * 26,
],
)
def test_parse_rejects_malformed_ids(malformed: str):
with pytest.raises(ValueError):
parse_aurora_id(malformed)
assert is_valid_aurora_id(malformed) is False
def test_aurora_ids_sort_in_creation_order():
# UUIDv7 is time-ordered, and the Crockford base32 encoding preserves
# byte order, so IDs generated later should sort later.
first = aurora_id("movie", new_uuid7())
second = aurora_id("movie", new_uuid7())
assert first < second
+184
View File
@@ -0,0 +1,184 @@
"""Unit tests for the TMDb normalizer (app/services/metadata/normalizer.py)
— section 13. Uses hand-built payload fragments shaped like real TMDb
responses rather than calling the network."""
from __future__ import annotations
from datetime import date
from app.services.metadata.normalizer import normalize_movie, normalize_person
FAKE_MOVIE = {
"id": 603,
"title": "The Matrix",
"original_title": "The Matrix",
"overview": "A computer hacker learns...",
"tagline": "Welcome to the Real World",
"status": "Released",
"original_language": "en",
"release_date": "1999-03-30",
"runtime": 136,
"budget": 63000000,
"revenue": 463517383,
"popularity": 45.5,
"vote_average": 8.2,
"vote_count": 24000,
"adult": False,
"imdb_id": "tt0133093",
"genres": [{"id": 28, "name": "Action"}, {"id": 878, "name": "Science Fiction"}],
"production_companies": [
{"id": 79, "name": "Village Roadshow Pictures", "origin_country": "US"}
],
"poster_path": "/poster.jpg",
"backdrop_path": "/backdrop.jpg",
"belongs_to_collection": {"id": 2344, "name": "The Matrix Collection"},
"credits": {
"cast": [
{
"id": 6384,
"name": "Keanu Reeves",
"character": "Neo",
"order": 0,
"profile_path": "/keanu.jpg",
"gender": 2,
}
],
"crew": [
{
"id": 9339,
"name": "Lana Wachowski",
"department": "Directing",
"job": "Director",
"gender": 1,
}
],
},
"images": {
"posters": [
{"file_path": "/poster.jpg", "width": 2000, "height": 3000, "aspect_ratio": 0.667, "iso_639_1": "en", "vote_average": 8.1}
],
"backdrops": [],
"logos": [],
},
"videos": {
"results": [
{
"id": "abc123",
"type": "Trailer",
"site": "YouTube",
"key": "vKQi3bBA1y8",
"iso_639_1": "en",
"iso_3166_1": "US",
"official": True,
"published_at": "1999-03-01T00:00:00.000Z",
}
]
},
"keywords": {"keywords": [{"id": 1, "name": "dystopia"}]},
"alternative_titles": {"titles": [{"title": "Matrix", "iso_3166_1": "US"}]},
"external_ids": {"imdb_id": "tt0133093"},
"recommendations": {"results": [{"id": 604}]},
}
FAKE_PERSON = {
"id": 6384,
"name": "Keanu Reeves",
"biography": "Keanu Charles Reeves is a Canadian actor.",
"birthday": "1964-09-02",
"deathday": None,
"place_of_birth": "Beirut, Lebanon",
"gender": 2,
"known_for_department": "Acting",
"popularity": 30.2,
"also_known_as": ["Keanu Charles Reeves"],
"profile_path": "/keanu.jpg",
"images": {"profiles": [{"file_path": "/keanu.jpg", "width": 1000, "height": 1500, "aspect_ratio": 0.667, "iso_639_1": None, "vote_average": 5.0}]},
"external_ids": {"imdb_id": "nm0000206"},
}
def test_normalize_movie_maps_core_fields():
movie = normalize_movie(FAKE_MOVIE)
assert movie.external_id == "603"
assert movie.title == "The Matrix"
assert movie.status == "released"
assert movie.release_date == date(1999, 3, 30)
assert movie.runtime_minutes == 136
assert movie.budget == 63000000
assert movie.adult is False
def test_normalize_movie_maps_genres_and_keywords():
movie = normalize_movie(FAKE_MOVIE)
assert movie.genres == ["Action", "Science Fiction"]
assert movie.keywords == ["dystopia"]
assert movie.alternate_titles == ["Matrix"]
def test_normalize_movie_maps_cast_and_crew():
movie = normalize_movie(FAKE_MOVIE)
assert len(movie.cast) == 1
assert movie.cast[0].external_id == "6384"
assert movie.cast[0].department == "Acting"
assert movie.cast[0].character == "Neo"
assert movie.cast[0].gender == "male"
assert len(movie.crew) == 1
assert movie.crew[0].department == "Directing"
assert movie.crew[0].job == "Director"
assert movie.crew[0].gender == "female"
def test_normalize_movie_maps_artwork_and_videos():
movie = normalize_movie(FAKE_MOVIE)
poster_types = [a.artwork_type for a in movie.artwork]
assert poster_types.count("poster") >= 1
assert all(a.url.startswith("https://image.tmdb.org/t/p/original/") for a in movie.artwork)
assert len(movie.videos) == 1
assert movie.videos[0].url == "https://www.youtube.com/watch?v=vKQi3bBA1y8"
assert movie.videos[0].is_official is True
def test_normalize_movie_external_ids_never_include_tmdb_itself():
# TMDb's own id is handled separately by the ingestion pipeline via
# `normalized.external_id`, not folded into `external_ids` (section 3:
# Aurora doesn't conflate its own identity with provider identity).
movie = normalize_movie(FAKE_MOVIE)
assert "tmdb" not in movie.external_ids
assert movie.external_ids["imdb"] == "tt0133093"
def test_normalize_movie_maps_collection_and_recommendations():
movie = normalize_movie(FAKE_MOVIE)
assert movie.collection == ("2344", "The Matrix Collection")
assert movie.recommendation_external_ids == ["604"]
def test_normalize_movie_handles_missing_optional_sections():
minimal = {"id": 1, "title": "Minimal Movie"}
movie = normalize_movie(minimal)
assert movie.title == "Minimal Movie"
assert movie.genres == []
assert movie.cast == []
assert movie.artwork == []
assert movie.collection is None
def test_normalize_person_maps_core_fields():
person = normalize_person(FAKE_PERSON)
assert person.external_id == "6384"
assert person.name == "Keanu Reeves"
assert person.birthday == date(1964, 9, 2)
assert person.deathday is None
assert person.gender == "male"
assert person.external_ids["imdb"] == "nm0000206"
assert "Keanu Charles Reeves" in person.aliases
def test_normalize_person_profile_artwork_deduplicated():
person = normalize_person(FAKE_PERSON)
# profile_path duplicates an entry already in images.profiles; it must
# not be inserted twice.
paths = [a.provider_asset_id for a in person.artwork]
assert paths.count("/keanu.jpg") == 1