Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ jobs:
scripts/e2e/scenarios/01-basic-compress.json \
scripts/e2e/scenarios/02-quality-reject.json \
scripts/e2e/scenarios/03-quality-acknowledge.json \
scripts/e2e/scenarios/04-batch-compress.json
scripts/e2e/scenarios/04-batch-compress.json \
scripts/e2e/scenarios/05-subagent-compress.json \
scripts/e2e/scenarios/06-nudge-triggered.json \
scripts/e2e/scenarios/07-protection-filtered.json \
scripts/e2e/scenarios/08-nudge-with-protection.json
- name: Dump diagnostics on failure
if: failure()
run: |
Expand Down
33 changes: 33 additions & 0 deletions devlog/2026-07-27_fix-e2e-tests/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# REQ: Fix E2E Tests — Not "形同虚设"

## Problem

The Docker E2E tests (`scripts/e2e/`) had severe gaps that made them ineffective at catching real bugs:

1. **ALL protection disabled in E2E config** — `preserveRecentMessages: 0, preserveRecentTokens: 0, preserveLastUserMessage: false`. The protection mechanism fixed 3 times (PR #210/#211/#212) was completely untested.

2. **CI ran only 4/6 scenarios** — scenarios 05 (subagent) and 06 (nudge-triggered) existed but were NOT included in the CI `run-e2e.sh` invocation.

3. **Would NOT catch recent bugs**:
- Baseline-reset (PR #207): only triggers with `preserveRecentMessages > 0`
- Giant group (PR #210): only in autonomous sessions
- Hard-reject protection (PR #212): E2E disabled all protection

4. **verify.ts too shallow** — only checked `blockCount`. No verification of which messages were compressed vs protected.

5. **No Docker** — despite AGENTS.md §5.7.2 calling them "Docker E2E tests", no Dockerfile existed.

## Requirements

1. Add per-scenario config override support so scenarios can test production-like protection settings
2. Add scenario 07: protection-filtered — tests compress with `preserveRecentMessages: 5`, verifies protected messages excluded from compressed set
3. Add scenario 08: nudge-with-protection — tests nudge→compress flow WITH protection enabled (the exact bug scenario from PR #210/#212)
4. Deepen verify.ts with `compressedCount` / `minCompressedCount` / `maxCompressedCount` checks
5. Add all scenarios (05-08) to CI `ci.yml`
6. Update README with new scenario format documentation

## Acceptance Criteria

- All 8 E2E scenarios pass locally
- Build + typecheck + 922 unit tests still pass
- CI runs all 8 scenarios
47 changes: 47 additions & 0 deletions devlog/2026-07-27_fix-e2e-tests/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# WORKLOG: Fix E2E Tests

## Changes

### `scripts/e2e/run-e2e.sh`
- Extracted hardcoded `acp.jsonc` into `write_acp_config()` function
- Added per-scenario config override: reads `acpConfig` field from scenario JSON, writes it as the config for that scenario
- Default config unchanged (all protection disabled) — backward compatible with existing scenarios

### `scripts/e2e/verify.ts`
- Added `compressedCount`, `minCompressedCount`, `maxCompressedCount` to `VerifyExpectations`
- Added `getCompressedMessageIds()` — reads `prune.messages.byMessageId` keys (the compressed message set)
- These checks verify that protection actually filtered messages, not just that compress succeeded

### `scripts/e2e/scenarios/07-protection-filtered.json` (NEW)
- Production config: `preserveRecentMessages: 5, preserveLastUserMessage: true`
- 8 text turns (infrastructure topics) + compress "all"
- Verifies: `blockCount: 1` (compress succeeded, soft-filter not hard-reject) + `maxCompressedCount: 14` (at least some messages protected)
- Would FAIL on pre-v1.14.2 code (hard-reject → `blockCount: 0`)

### `scripts/e2e/scenarios/08-nudge-with-protection.json` (NEW)
- Production config: `preserveRecentMessages: 5, preserveLastUserMessage: true`
- 5 `nudge-compress` turns with large growth text
- Verifies: `minBlockCount: 1` (compress succeeded) + `nudgeBaselineSet: true` (baseline not reset) + `maxCompressedCount: 10` (protection working)
- Tests the exact bug scenario from PR #207/#210/#212

### `.github/workflows/ci.yml`
- Added scenarios 05, 06, 07, 08 to the E2E job

### `scripts/e2e/README.md`
- Updated scenario table with scenarios 07 + 08
- Documented `acpConfig` field and new verify fields

## Verification

- Build: ✅ (454 KB bundle)
- Typecheck: ✅ (0 errors)
- Unit tests: ✅ (922/922 pass)
- E2E local: ✅ (8/8 scenarios pass)
- 01-basic-compress: 1 block ✅
- 02-quality-reject: 0 blocks ✅
- 03-quality-acknowledge: 1 block ✅
- 04-batch-compress: 3 blocks ✅
- 05-subagent-compress: parent 0 + child 1 ✅
- 06-nudge-triggered: 1 block + baseline set ✅
- 07-protection-filtered: 1 block + compressedCount ≤ 14 ✅
- 08-nudge-with-protection: 1 block + baseline set + compressedCount ≤ 10 ✅
5 changes: 5 additions & 0 deletions scripts/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ the turn counter for real conversation turns.
| `04-batch-compress.json` | 4 text turns → batch compress 3 ranges → verify 3 blocks |
| `05-subagent-compress.json` | Subagent session → compress → verify parent + child blocks |
| `06-nudge-triggered.json` | Text turns grow context → ACP auto-injects nudge → fake LLM detects nudge and compresses → verify block count + nudge baseline |
| `07-protection-filtered.json` | Production config (preserveRecentMessages:5) → compress all → verify protected messages excluded from compressed set (soft-filter, not hard-reject) |
| `08-nudge-with-protection.json` | Nudge→compress WITH protection enabled → verify compress succeeds despite protected zone, nudge baseline set, protected messages survived |

### Scenario Format

Expand Down Expand Up @@ -102,6 +104,9 @@ the turn counter for real conversation turns.
- `retryOnReject`: if the compress is rejected by quality gate, retry with this config
- `ranges`: array for batch compress (multiple ranges in one call)
- `growthText`: for `nudge-compress` — text emitted when no nudge detected (grows context until ACP injects nudge)
- `acpConfig`: optional — overrides the default `acp.jsonc` for this scenario (enables testing protection behavior)
- `verify.blockCount`: exact block count after scenario
- `verify.minBlockCount`: minimum block count
- `verify.nudgeBaselineSet`: `true` = `lastPerMessageNudgeTokens` is set (not null/undefined)
- `verify.compressedCount`: exact count of messages in `byMessageId` (compressed set)
- `verify.minCompressedCount` / `verify.maxCompressedCount`: inclusive bounds on compressed message count
54 changes: 36 additions & 18 deletions scripts/e2e/run-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,35 @@ mkdir -p "$FAKE_HOME/.config/opencode"
ACP_DIST="$REPO_ROOT/dist"
[[ -f "$ACP_DIST/index.js" ]] || fail "ACP dist not found at $ACP_DIST — run npm run build"

# write_acp_config [output-path] [override-jsonc-string] — defaults disable all protection (legacy); scenarios override via "acpConfig" field
write_acp_config() {
local out="${1:-$FAKE_HOME/.config/opencode/acp.jsonc}"
local override="${2:-}"

if [[ -n "$override" ]]; then
printf '%s\n' "$override" > "$out"
else
cat > "$out" <<'ACPJSON'
{
"experimental": { "allowSubAgents": true },
"compress": {
"minCompressRange": 0,
"maxSummaryLengthHard": 20000,
"preserveRecentMessages": 0,
"preserveRecentTokens": 0,
"preserveLastUserMessage": false,
"maxContextLimit": 20000,
"minContextLimit": 10000
},
"qualityGate": {
"enabled": true,
"algorithm": "rouge-recall-v1"
}
}
ACPJSON
fi
}

cat > "$FAKE_HOME/.config/opencode/opencode.json" <<OCJSON
{
"\$schema": "https://opencode.ai/config.json",
Expand Down Expand Up @@ -114,24 +143,7 @@ cat > "$FAKE_HOME/.config/opencode/opencode.json" <<OCJSON
}
OCJSON

cat > "$FAKE_HOME/.config/opencode/acp.jsonc" <<'ACPJSON'
{
"experimental": { "allowSubAgents": true },
"compress": {
"minCompressRange": 0,
"maxSummaryLengthHard": 20000,
"preserveRecentMessages": 0,
"preserveRecentTokens": 0,
"preserveLastUserMessage": false,
"maxContextLimit": 20000,
"minContextLimit": 10000
},
"qualityGate": {
"enabled": true,
"algorithm": "rouge-recall-v1"
}
}
ACPJSON
write_acp_config "$FAKE_HOME/.config/opencode/acp.jsonc"
pass "opencode config written"

step "warm up opencode DB (migration takes time on first run)"
Expand All @@ -145,6 +157,12 @@ for scenario in "${SCENARIOS[@]}"; do
scenario_name=$(basename "$scenario" .json)
step "scenario: $scenario_name"

scenario_acp_config=$("$NODE_BIN" -e "
const s = JSON.parse(require('fs').readFileSync('$scenario','utf8'));
process.stdout.write(s.acpConfig ? JSON.stringify(s.acpConfig, null, 4) : '');
" 2>/dev/null || echo "")
write_acp_config "$FAKE_HOME/.config/opencode/acp.jsonc" "$scenario_acp_config"

rm -rf "$FAKE_HOME/.local/share/opencode/storage/plugin/acp"
rm -f "$FAKE_HOME/.local/share/opencode/opencode.db"
env $OC_ENV timeout -s KILL 120 "$OPENCODE_BIN" session list </dev/null >/dev/null 2>&1 || true
Expand Down
69 changes: 69 additions & 0 deletions scripts/e2e/scenarios/07-protection-filtered.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "protection-filtered",
"description": "Production config (preserveRecentMessages:5, preserveLastUserMessage:true) — compress 'all' should soft-filter protected messages, not hard-reject. Verifies block created AND protected messages excluded from compressed set.",
"acpConfig": {
"experimental": { "allowSubAgents": true },
"compress": {
"minCompressRange": 0,
"maxSummaryLengthHard": 20000,
"preserveRecentMessages": 5,
"preserveRecentTokens": 0,
"preserveLastUserMessage": true,
"maxContextLimit": 20000,
"minContextLimit": 10000
},
"qualityGate": {
"enabled": true,
"algorithm": "rouge-recall-v1"
}
},
"turns": [
{
"respond": "text",
"text": "Topic one: Redis caching architecture. Redis is an in-memory key-value store used for caching session management and message queues. It supports data structures like strings hashes lists sets and sorted sets. Persistence options include RDB snapshots and AOF logs. The eviction policy uses LRU least recently used to remove stale entries when memory is full. The Redis client is initialized at src/cache/redis.ts line 10 with connection pooling configured for maximum 50 connections. Cache invalidation strategies include TTL-based expiration write-through caching and explicit invalidation via DEL command. The cache layer sits between the application and PostgreSQL database reducing query latency from 50ms to 2ms for hot paths."
},
{
"respond": "text",
"text": "Topic two: Kafka event streaming. Apache Kafka is a distributed event streaming platform for building real-time data pipelines. Core concepts include producers which publish messages to topics consumers which subscribe to topics partitions which enable parallelism and consumer groups which enable scalable consumption. The Kafka producer is configured at src/messaging/kafka.ts line 20 with acks set to all for maximum durability. Message ordering is guaranteed within a partition but not across partitions. The consumer group coordinator at src/messaging/consumer.ts line 35 manages rebalancing when consumers join or leave the group."
},
{
"respond": "text",
"text": "Topic three: GraphQL API design. GraphQL provides a query language for APIs that lets clients request exactly the data they need. Key concepts include schema definition language resolvers mutations for writes and subscriptions for real-time updates. Unlike REST a single endpoint handles all queries. The schema is defined at src/api/graphql.ts line 15 using type definitions for Query Mutation and Subscription types. N+1 query prevention uses DataLoader at src/api/dataloader.ts line 8 which batches and caches database requests within a single request cycle. Authorization is enforced via field-level resolvers that check user permissions before returning sensitive data."
},
{
"respond": "text",
"text": "Topic four: Docker containerization. Docker containers package applications with their dependencies for consistent deployment across environments. Multi-stage builds reduce final image size by separating build dependencies from runtime dependencies. Docker Compose orchestrates multi-container applications defining services networks and volumes in a single YAML file. Key practices include using non-root users in containers minimal base images like alpine or distroless health checks for orchestration integration and layer caching for faster builds. The Dockerfile at Dockerfile line 1 defines a multi-stage build starting from node 22 alpine building the TypeScript source and copying only the compiled output to the final image."
},
{
"respond": "text",
"text": "Topic five: Kubernetes orchestration. Kubernetes manages containerized applications across clusters of machines. Key resources include Pods which are the smallest deployable units Deployments which manage replica sets and Services which provide stable network endpoints. ConfigMaps and Secrets manage configuration and sensitive data separately from container images. Horizontal Pod Autoscaler scales replicas based on CPU memory or custom metrics. The deployment manifest at k8s/deployment.yaml defines 3 replicas with resource limits of 512Mi memory and 500m CPU. Rolling updates are configured with maxSurge 1 and maxUnavailable 0 ensuring zero downtime during deployments."
},
{
"respond": "text",
"text": "Topic six: Observability and monitoring. The monitoring stack uses Prometheus for metrics collection Grafana for visualization and Loki for log aggregation. Prometheus scrapes metrics from application endpoints at /metrics every 15 seconds. Custom business metrics are instrumented at src/metrics/business.ts using prom-client library. Grafana dashboards at grafana/dashboards/ display service health request latency error rates and business KPIs. Distributed tracing uses OpenTelemetry with traces exported to Jaeger. Alert rules at grafana/alerts/ trigger PagerDuty notifications for critical issues like error rate above 5 percent or p99 latency above 500ms."
},
{
"respond": "text",
"text": "Topic seven: CI CD pipeline. The CI CD pipeline uses GitHub Actions for continuous integration and ArgoCD for continuous deployment. On every pull request the CI workflow runs lint typecheck unit tests and integration tests. On merge to main the build workflow creates a Docker image pushes it to the registry and updates the Kubernetes manifest repository. ArgoCD detects the manifest change and syncs the new version to the cluster using a blue-green deployment strategy. Rollback is automated if health checks fail within the first 5 minutes. The pipeline configuration lives at .github/workflows/ci.yml and .github/workflows/deploy.yml."
},
{
"respond": "text",
"text": "Topic eight: Database migrations. Database schema changes are managed through versioned migration files using a tool called golang-migrate. Each migration is numbered sequentially and can include both up and down scripts for forward and rollback operations. Migration files live at db/migrations/ with naming convention 001_initial_schema_up.sql and 001_initial_schema_down.sql. The migration runner at scripts/migrate.ts applies pending migrations on application startup before the server begins accepting traffic. Zero-downtime migrations require careful planning including expand-then-contract patterns where new columns are added first then backfilled then old columns are removed in a subsequent deployment."
},
{
"respond": "compress",
"topic": "Infrastructure Discussion",
"summary": "## Infrastructure Discussion\n\nDiscussed eight infrastructure topics:\n\n### Redis Caching\nIn-memory key-value store for caching sessions and queues. Supports strings hashes lists sets sorted sets. Persistence via RDB and AOF. Client init at src/cache/redis.ts:10 with 50 connection pool. LRU eviction. TTL write-through and DEL invalidation. Reduces query latency from 50ms to 2ms.\n\n### Kafka Event Streaming\nDistributed event streaming. Producers publish to topics consumers subscribe partitions for parallelism consumer groups for scalable consumption. Producer at src/messaging/kafka.ts:20 with acks=all. Ordering guaranteed within partition. Consumer coordinator at src/messaging/consumer.ts:35.\n\n### GraphQL API Design\nQuery language for precise data fetching. Schema at src/api/graphql.ts:15. DataLoader at src/api/dataloader.ts:8 prevents N+1. Field-level resolvers enforce authorization.\n\n### Docker Containerization\nMulti-stage builds separate build from runtime deps. Dockerfile at Dockerfile:1 uses node 22 alpine. Non-root users minimal base images health checks layer caching.\n\n### Kubernetes Orchestration\nPods Deployments Services ConfigMaps Secrets. HPA scales on CPU memory custom metrics. k8s/deployment.yaml defines 3 replicas 512Mi 500m limits. Rolling updates maxSurge 1 maxUnavailable 0.\n\n### Observability\nPrometheus metrics Grafana dashboards Loki logs OpenTelemetry traces to Jaeger. Custom metrics at src/metrics/business.ts. Alerts trigger PagerDuty for error rate above 5 percent or p99 above 500ms.\n\n### CI CD Pipeline\nGitHub Actions for CI lint typecheck tests. ArgoCD for CD blue-green deployment. Config at .github/workflows/ci.yml and deploy.yml. Automated rollback if health checks fail within 5 minutes.\n\n### Database Migrations\ngolang-migrate with versioned files at db/migrations/. Naming 001_initial_schema_up.sql. Runner at scripts/migrate.ts applies on startup. Zero-downtime via expand-then-contract pattern.",
"range": "all"
},
{
"respond": "text",
"text": "Compression complete with protection filtering applied.",
"auto": true
}
],
"verify": {
"blockCount": 1,
"maxCompressedCount": 14
}
}
Loading
Loading