Skip to content

Commit ae56afd

Browse files
committed
feat: add AGENTS.md and test-extension agent skill
Add repo-wide agent instructions (AGENTS.md) and an experimental test-extension skill (.skills/test-extension/) for full-lifecycle manual testing of Firebase extensions against the local emulator. Includes helper scripts for emulator management, Firestore read/write, and status polling, plus reference docs for all extension types.
1 parent d148877 commit ae56afd

File tree

11 files changed

+544
-0
lines changed

11 files changed

+544
-0
lines changed

.skills/test-extension/SKILL.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
name: test-extension
3+
description: Test Firebase extensions end-to-end using the local emulator. Use when the user asks to test, verify, or debug an extension — covers building, emulator setup, writing test data, triggering, and verifying output.
4+
---
5+
6+
# Test Extension
7+
8+
Full-lifecycle manual testing of Firebase extensions against the local emulator.
9+
10+
## Quick Start
11+
12+
```bash
13+
# 1. Build the extension
14+
cd <extension>/functions && npm install && npm run build
15+
16+
# 2. Start the emulator (from repo root)
17+
.skills/test-extension/scripts/start-emulator.sh
18+
19+
# 3. Write test data to trigger the extension
20+
.skills/test-extension/scripts/write-firestore-doc.sh <collection> '<json>'
21+
22+
# 4. Watch for completion
23+
.skills/test-extension/scripts/watch-status.sh <collection> <doc-id>
24+
25+
# 5. Read the result
26+
.skills/test-extension/scripts/read-firestore-doc.sh <collection>/<doc-id>
27+
28+
# 6. Clean up
29+
.skills/test-extension/scripts/clear-firestore.sh
30+
.skills/test-extension/scripts/stop-emulator.sh
31+
```
32+
33+
## Extension Type Routing
34+
35+
Before testing, determine the extension's trigger type:
36+
37+
- **Firestore-triggered** (most GenAI extensions): Write a document to the configured collection. See [references/firestore-extensions.md](references/firestore-extensions.md) for per-extension details.
38+
- **Storage-triggered** (image/video/audio extensions): Upload a file to the configured bucket. See [references/storage-extensions.md](references/storage-extensions.md).
39+
- **Other** (Pub/Sub, HTTPS): See the extension's own test files for patterns.
40+
41+
## Firestore Extension Testing Flow
42+
43+
1. Write a document with the required input fields (e.g. `prompt` for chatbot)
44+
2. The extension triggers on the write and sets `status.state` to `PROCESSING`
45+
3. On completion, `status.state` becomes `COMPLETED` and the response field is populated
46+
4. If it fails, `status.state` becomes `ERRORED` with `status.error` containing the message
47+
48+
**Gotchas:**
49+
- The document must NOT already have the response field populated, or the extension skips it
50+
- The document must NOT already have `status.state` set to `PROCESSING`, `COMPLETED`, or `ERRORED`
51+
- To re-trigger, either create a new document or clear the response field and status
52+
53+
## Storage Extension Testing Flow
54+
55+
1. Upload a file to the configured storage bucket via the emulator REST API or `gsutil`
56+
2. The extension triggers on `object.finalize`
57+
3. Output is typically written to a Firestore collection or a different storage bucket
58+
4. Check Firestore or the output bucket for results
59+
60+
## Emulator Details
61+
62+
See [references/emulator.md](references/emulator.md) for port mapping, health checks, environment variables, and Firebase CLI commands.
63+
64+
## Available Scripts
65+
66+
All scripts are in `scripts/` and use `curl` + `jq` against the emulator REST API:
67+
68+
| Script | Purpose |
69+
|--------|---------|
70+
| `start-emulator.sh` | Start emulator with health check polling |
71+
| `stop-emulator.sh` | Stop emulator cleanly |
72+
| `clear-firestore.sh` | Delete all Firestore emulator data |
73+
| `write-firestore-doc.sh` | Write a JSON document to a collection |
74+
| `read-firestore-doc.sh` | Read a document or list a collection |
75+
| `watch-status.sh` | Poll a document until status.state matches a target |
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Firebase Emulator Reference
2+
3+
## Port Mapping
4+
5+
| Service | Port | URL |
6+
|------------|------|-----|
7+
| Firestore | 8080 | http://127.0.0.1:8080 |
8+
| Storage | 9199 | http://127.0.0.1:9199 |
9+
| Auth | 9099 | http://127.0.0.1:9099 |
10+
| Pub/Sub | 8085 | http://127.0.0.1:8085 |
11+
| Functions | 5001 | http://127.0.0.1:5001 |
12+
| Hub/UI | 4000 | http://127.0.0.1:4000 |
13+
| Hosting | 8081 | http://127.0.0.1:8081 |
14+
15+
## Health Check
16+
17+
The emulator hub exposes a health endpoint:
18+
19+
```bash
20+
curl -sf http://127.0.0.1:4000 > /dev/null && echo "Emulator is running" || echo "Emulator is not running"
21+
```
22+
23+
## Environment Variables
24+
25+
Tests expect these environment variables to point to the emulator:
26+
27+
```bash
28+
export FIRESTORE_EMULATOR_HOST="127.0.0.1:8080"
29+
export STORAGE_EMULATOR_HOST="127.0.0.1:9199"
30+
export FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099"
31+
export PUBSUB_EMULATOR_HOST="127.0.0.1:8085"
32+
export GCLOUD_PROJECT="demo-gcp"
33+
export PROJECT_ID="demo-gcp"
34+
```
35+
36+
## Emulator Configuration
37+
38+
The emulator config lives in `_emulator/`:
39+
40+
```
41+
_emulator/
42+
├── firebase.json # Emulator service config and ports
43+
├── .firebaserc # Project aliases (demo-extensions-testing, dev-extensions-testing)
44+
├── firestore.rules # Firestore security rules
45+
├── storage.rules # Storage security rules
46+
├── extensions/ # Extension environment configs
47+
│ ├── firestore-palm-chatbot.env
48+
│ ├── firestore-semantic-search.env
49+
│ ├── storage-image-labeling.env.local
50+
│ └── ...
51+
└── functions/ # Dummy functions project for emulator
52+
├── index.js
53+
└── package.json
54+
```
55+
56+
## Extension Environment Config
57+
58+
Each extension can have an `.env` file in `_emulator/extensions/` that sets its params. Example for `firestore-semantic-search.env`:
59+
60+
```
61+
COLLECTION_NAME=products
62+
EMBEDDING_METHOD=palm
63+
FIELDS=title,description
64+
DISTANCE_MEASURE=SQUARED_L2_DISTANCE
65+
```
66+
67+
## Firebase CLI Commands
68+
69+
```bash
70+
# Start emulator
71+
cd _emulator && firebase emulators:start --project=demo-gcp
72+
73+
# Clear Firestore data
74+
curl -X DELETE "http://127.0.0.1:8080/emulator/v1/projects/demo-gcp/databases/(default)/documents"
75+
76+
# List Firestore documents
77+
curl -sf "http://127.0.0.1:8080/v1/projects/demo-gcp/databases/(default)/documents/<collection>" | jq .
78+
79+
# List Storage objects
80+
curl -sf "http://127.0.0.1:9199/v0/b/demo-gcp.appspot.com/o" | jq '.items[].name'
81+
```
82+
83+
## Clearing Data Between Tests
84+
85+
```bash
86+
# Clear all Firestore data
87+
.skills/test-extension/scripts/clear-firestore.sh
88+
89+
# Clear a specific collection (delete and recreate)
90+
# There's no emulator API for this — clear all data instead
91+
92+
# Clear Storage (no emulator API — restart emulator or delete objects individually)
93+
```
94+
95+
## Troubleshooting
96+
97+
- **Port already in use**: Run `npx kill-port 8080 8085 4000 4400 5001 9199 9000 9099`
98+
- **Emulator won't start**: Check Java is installed (`java -version`), the emulator requires it
99+
- **Tests fail with ECONNREFUSED**: Emulator isn't running or wrong port. Check `FIRESTORE_EMULATOR_HOST`
100+
- **Extension doesn't trigger**: Check that the extension's env file exists in `_emulator/extensions/` and has the correct `COLLECTION_NAME` or bucket config
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Firestore-Triggered Extensions
2+
3+
## Extension Reference
4+
5+
| Extension | Collection (default) | Input | Response Field | Trigger |
6+
|-----------|---------------------|-------|---------------|---------|
7+
| firestore-multimodal-genai | `generate` | Handlebars vars from PROMPT + optional IMAGE_FIELD | `output` | document.write |
8+
| firestore-genai-chatbot | `generate` | `prompt` field | `response` | document.write |
9+
| firestore-vector-search | `products` | `input` field | `embedding` | document.write |
10+
| firestore-semantic-search | (configured at install) | Fields from FIELDS param | Vertex AI index | document.create |
11+
| firestore-palm-chatbot | `users/{uid}/discussions/{id}/messages` | `prompt` field | `response` | document.write |
12+
| firestore-palm-gen-text | `generate` | Handlebars vars from PROMPT | `output` | document.write |
13+
| firestore-palm-summarize-text | `text_documents` | `text` field | `summary` | document.write |
14+
| firestore-incremental-capture | (wildcard `{document=**}`) | Any document fields | BigQuery export | document.write |
15+
| text-to-speech | (configured at install) | `text` field | Audio in Cloud Storage | document.write |
16+
17+
## Example Test Workflows
18+
19+
### firestore-multimodal-genai
20+
21+
The extension substitutes handlebars variables from the document into the configured PROMPT.
22+
23+
```bash
24+
# If PROMPT is "What is the capital of {{ country }}?"
25+
./scripts/write-firestore-doc.sh generate '{"country": "France"}'
26+
27+
# If PROMPT is a static prompt with no variables
28+
./scripts/write-firestore-doc.sh generate '{"dummy": "trigger"}'
29+
```
30+
31+
The extension will:
32+
1. Set `status.state` to `PROCESSING`
33+
2. Call Gemini API with the substituted prompt
34+
3. Set `status.state` to `COMPLETED` and populate the `output` field
35+
36+
```bash
37+
# Watch for completion (use the doc ID from write output)
38+
./scripts/watch-status.sh generate/<doc-id> COMPLETED
39+
40+
# Read the result
41+
./scripts/read-firestore-doc.sh generate/<doc-id>
42+
```
43+
44+
### firestore-genai-chatbot
45+
46+
The chatbot extension expects a `prompt` field and writes to `response`.
47+
48+
```bash
49+
./scripts/write-firestore-doc.sh generate '{"prompt": "Hello, how are you?"}'
50+
./scripts/watch-status.sh generate/<doc-id> COMPLETED
51+
./scripts/read-firestore-doc.sh generate/<doc-id>
52+
```
53+
54+
For multi-turn conversations, documents are ordered by `createTime` within the collection path. Each new document in the same subcollection continues the conversation.
55+
56+
### firestore-vector-search
57+
58+
Expects an `input` field and generates an `embedding` field.
59+
60+
```bash
61+
./scripts/write-firestore-doc.sh products '{"input": "A comfortable ergonomic office chair"}'
62+
./scripts/watch-status.sh products/<doc-id> COMPLETED
63+
```
64+
65+
## Common Gotchas
66+
67+
- **Document won't trigger**: The response field (e.g. `output`) must NOT already exist on the document
68+
- **Status blocking**: If `status.state` is already `PROCESSING`, `COMPLETED`, or `ERRORED`, the extension skips the document
69+
- **Re-triggering**: Delete the document and create a new one, or clear both the response field and the status field
70+
- **Handlebars**: If the PROMPT uses `{{ variable }}`, the document MUST have a field named `variable` with a string value
71+
- **Missing variables**: The extension will error if a handlebars variable is referenced in PROMPT but missing from the document
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Storage-Triggered Extensions
2+
3+
## Extension Reference
4+
5+
| Extension | Bucket Param | Input | Output | Trigger |
6+
|-----------|-------------|-------|--------|---------|
7+
| storage-label-images | `IMG_BUCKET` | Image file (PNG, JPG) | Firestore `imageLabels` collection | object.finalize |
8+
| storage-label-videos | `INPUT_VIDEOS_BUCKET` | Video file | JSON in `OUTPUT_BUCKET` | object.finalize |
9+
| storage-extract-image-text | `IMG_BUCKET` | Image file (PNG, JPG) | Firestore `extractedText` collection | object.finalize |
10+
| storage-reverse-image-search | `IMG_BUCKET` | Image file | Vertex AI index embeddings | object.finalize + object.delete |
11+
| storage-transcode-videos | `INPUT_VIDEOS_BUCKET` | Video file | Transcoded video in `OUTPUT_VIDEOS_BUCKET` | object.finalize |
12+
| speech-to-text | `EXTENSION_BUCKET` | Audio file (WAV, FLAC, MP3) | `.txt` file in Storage + optional Firestore | object.finalize |
13+
14+
## Uploading Files to the Storage Emulator
15+
16+
The Storage emulator runs on port 9199. You can upload files using the Firebase Storage REST API:
17+
18+
```bash
19+
BUCKET="demo-gcp.appspot.com"
20+
FILE_PATH="test-image.png"
21+
STORAGE_PATH="images/test.png"
22+
23+
# Upload via the Storage emulator REST API
24+
curl -X POST \
25+
"http://127.0.0.1:9199/v0/b/${BUCKET}/o?name=${STORAGE_PATH}" \
26+
-H "Content-Type: image/png" \
27+
--data-binary @"${FILE_PATH}"
28+
```
29+
30+
You can also use `gsutil` with the emulator:
31+
32+
```bash
33+
export STORAGE_EMULATOR_HOST="http://127.0.0.1:9199"
34+
gsutil cp test-image.png gs://demo-gcp.appspot.com/images/test.png
35+
```
36+
37+
## Verifying Output
38+
39+
### Firestore Output (label-images, extract-image-text)
40+
41+
These extensions write results to a Firestore collection:
42+
43+
```bash
44+
# Check for labeled image results
45+
./scripts/read-firestore-doc.sh imageLabels
46+
47+
# Check for extracted text results
48+
./scripts/read-firestore-doc.sh extractedText
49+
```
50+
51+
### Storage Output (label-videos, transcode-videos)
52+
53+
These extensions write output files to a storage bucket. List bucket contents:
54+
55+
```bash
56+
curl -sf "http://127.0.0.1:9199/v0/b/demo-gcp.appspot.com/o" | jq '.items[].name'
57+
```
58+
59+
## Test Fixtures
60+
61+
Some extensions have test fixtures committed to the repo:
62+
63+
- `storage-label-images/functions/__tests__/fixtures/test.png`
64+
- `speech-to-text/functions/__tests__/fixtures/test.wav`
65+
66+
You can use these for manual testing.
67+
68+
## Path Filtering
69+
70+
Several storage extensions support `INCLUDE_PATH_LIST` and `EXCLUDE_PATH_LIST` params. When testing, make sure your upload path matches the include filter (or doesn't match the exclude filter).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PROJECT_ID="${PROJECT_ID:-demo-gcp}"
5+
HOST="${FIRESTORE_EMULATOR_HOST:-127.0.0.1:8080}"
6+
7+
echo "Clearing all Firestore data (project: $PROJECT_ID)..."
8+
curl -sf -X DELETE "http://${HOST}/emulator/v1/projects/${PROJECT_ID}/databases/(default)/documents" > /dev/null
9+
echo "Firestore data cleared."
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage: read-firestore-doc.sh <path>
5+
# Examples:
6+
# read-firestore-doc.sh generate # list all docs in collection
7+
# read-firestore-doc.sh generate/abc123 # read a specific document
8+
9+
DOC_PATH="${1:?Usage: read-firestore-doc.sh <collection> or <collection/doc-id>}"
10+
11+
PROJECT_ID="${PROJECT_ID:-demo-gcp}"
12+
HOST="${FIRESTORE_EMULATOR_HOST:-127.0.0.1:8080}"
13+
BASE_URL="http://${HOST}/v1/projects/${PROJECT_ID}/databases/(default)/documents"
14+
15+
# Determine if this is a collection (odd segments) or document (even segments)
16+
SEGMENT_COUNT=$(echo "$DOC_PATH" | tr '/' '\n' | wc -l | tr -d ' ')
17+
18+
if [ $((SEGMENT_COUNT % 2)) -eq 1 ]; then
19+
# Odd segments = collection path, list documents
20+
echo "Listing documents in: ${DOC_PATH}"
21+
curl -sf "${BASE_URL}/${DOC_PATH}" | jq .
22+
else
23+
# Even segments = document path, get single document
24+
echo "Reading document: ${DOC_PATH}"
25+
curl -sf "${BASE_URL}/${DOC_PATH}" | jq .
26+
fi
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
5+
EMULATOR_DIR="$REPO_ROOT/_emulator"
6+
PROJECT_ID="${PROJECT_ID:-demo-gcp}"
7+
HEALTH_URL="http://127.0.0.1:4000"
8+
MAX_WAIT="${MAX_WAIT:-60}"
9+
10+
echo "Resetting emulator ports..."
11+
npx kill-port 8080 8085 4000 4400 5001 9199 9000 9099 2>/dev/null || true
12+
13+
echo "Starting Firebase emulator (project: $PROJECT_ID)..."
14+
cd "$EMULATOR_DIR"
15+
firebase emulators:start --project="$PROJECT_ID" &
16+
EMULATOR_PID=$!
17+
18+
echo "Waiting for emulator to be ready (max ${MAX_WAIT}s)..."
19+
elapsed=0
20+
while [ $elapsed -lt "$MAX_WAIT" ]; do
21+
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
22+
echo "Emulator is ready (took ${elapsed}s)"
23+
echo " Firestore: http://127.0.0.1:8080"
24+
echo " Storage: http://127.0.0.1:9199"
25+
echo " Hub UI: http://127.0.0.1:4000"
26+
echo " PID: $EMULATOR_PID"
27+
exit 0
28+
fi
29+
sleep 2
30+
elapsed=$((elapsed + 2))
31+
done
32+
33+
echo "ERROR: Emulator failed to start within ${MAX_WAIT}s"
34+
kill $EMULATOR_PID 2>/dev/null || true
35+
exit 1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
echo "Stopping Firebase emulator..."
5+
npx kill-port 8080 8085 4000 4400 5001 9199 9000 9099 2>/dev/null || true
6+
echo "Emulator stopped."

0 commit comments

Comments
 (0)