diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0196437d..d7420b94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | diff --git a/devlog/2026-07-27_fix-e2e-tests/REQ.md b/devlog/2026-07-27_fix-e2e-tests/REQ.md new file mode 100644 index 00000000..ac500b6e --- /dev/null +++ b/devlog/2026-07-27_fix-e2e-tests/REQ.md @@ -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 diff --git a/devlog/2026-07-27_fix-e2e-tests/WORKLOG.md b/devlog/2026-07-27_fix-e2e-tests/WORKLOG.md new file mode 100644 index 00000000..48f889c0 --- /dev/null +++ b/devlog/2026-07-27_fix-e2e-tests/WORKLOG.md @@ -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 ✅ diff --git a/scripts/e2e/README.md b/scripts/e2e/README.md index dc0f9b59..7f333423 100644 --- a/scripts/e2e/README.md +++ b/scripts/e2e/README.md @@ -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 @@ -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 diff --git a/scripts/e2e/run-e2e.sh b/scripts/e2e/run-e2e.sh index 5e9d6e2f..c6aaf863 100755 --- a/scripts/e2e/run-e2e.sh +++ b/scripts/e2e/run-e2e.sh @@ -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" < "$FAKE_HOME/.config/opencode/opencode.json" < "$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)" @@ -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 2>&1 || true diff --git a/scripts/e2e/scenarios/07-protection-filtered.json b/scripts/e2e/scenarios/07-protection-filtered.json new file mode 100644 index 00000000..09f05b25 --- /dev/null +++ b/scripts/e2e/scenarios/07-protection-filtered.json @@ -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 + } +} diff --git a/scripts/e2e/scenarios/08-nudge-with-protection.json b/scripts/e2e/scenarios/08-nudge-with-protection.json new file mode 100644 index 00000000..a7761e79 --- /dev/null +++ b/scripts/e2e/scenarios/08-nudge-with-protection.json @@ -0,0 +1,57 @@ +{ + "name": "nudge-with-protection", + "description": "Nudge→compress flow WITH production protection enabled (preserveRecentMessages:5, preserveLastUserMessage:true). Tests the exact bug fixed in PR #210/#212: nudge fires, model compresses, protected messages are soft-filtered (not hard-rejected). Verifies block created + nudge baseline set + protected messages survived.", + "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": "nudge-compress", + "growthText": "Authentication system design discussion. JWT tokens encode user claims with three parts header specifying algorithm payload containing user ID role expiration and custom claims and signature computed from header and payload using a secret key. The signature prevents tampering and ensures token integrity. File src/auth/jwt.ts handles token creation at line 15 and verification at line 42. The verifyToken function checks signature validity and expiration timestamp. If the token is expired or has an invalid signature the function throws an AuthenticationError which is caught by the error handling middleware at src/middleware/error.ts line 28. Refresh tokens allow obtaining new access tokens without requiring the user to re-enter credentials. The refresh endpoint at src/routes/auth.ts line 78 validates the refresh token issued during login and generates a new access token with a fresh expiration time. Token storage uses httpOnly secure cookies to prevent XSS attacks from stealing tokens via JavaScript.", + "topic": "Auth System", + "summary": "## Auth System Discussion\n\nDiscussed JWT authentication: token structure (header.payload.signature), verifyToken at src/auth/jwt.ts:42, refresh endpoint at src/routes/auth.ts:78. Security: httpOnly cookies, CSRF at src/middleware/csrf.ts:15, rate limiting at src/middleware/ratelimit.ts:30. Password hashing with bcrypt cost 12." + }, + { + "respond": "nudge-compress", + "growthText": "Database layer for user management and session storage. The users table stores columns id email password_hash created_at updated_at and last_login_at. The id column is a UUID primary key generated using uuid v7 for time-ordered uniqueness. The email column has a unique index for fast lookups during login. The password_hash column stores the bcrypt hash. Migration 001_initial_schema.sql creates the users table with all columns and indexes. Migration 002_add_sessions.sql creates the sessions table with columns id token user_id expires_at created_at and a foreign key to users.id with cascade delete. The User model at src/models/user.ts line 25 defines the schema with field types validations and relationships to sessions. The findByEmail query at src/db/queries/user.ts line 25 fetches a user by email address for login verification using parameterized queries to prevent SQL injection.", + "topic": "Auth System", + "summary": "## Auth System Discussion\n\nDiscussed JWT authentication: token structure (header.payload.signature), verifyToken at src/auth/jwt.ts:42, refresh endpoint at src/routes/auth.ts:78. Security: httpOnly cookies, CSRF at src/middleware/csrf.ts:15, rate limiting at src/middleware/ratelimit.ts:30." + }, + { + "respond": "nudge-compress", + "growthText": "API endpoints design and request handling for the authentication service. POST /api/auth/login accepts a JSON body with email and password fields validates them using zod schema at src/schemas/auth.ts line 10 then calls the login service which verifies credentials and returns JWT access token plus sets refresh token in httpOnly cookie. The response format follows JSON API specification with data and meta fields. POST /api/auth/refresh validates the refresh token from cookie and returns a new access token. The endpoint also rotates the refresh token for security meaning the old refresh token is invalidated. POST /api/auth/logout clears both access and refresh cookies and marks the session as inactive in the database. GET /api/auth/me returns the current user profile including id email role and permissions. Input validation middleware at src/middleware/validate.ts line 20 checks required fields data types string lengths email format and password complexity rules. Error responses follow RFC 7807 problem details format with type title status detail and instance fields.", + "topic": "Auth System", + "summary": "## Auth System Discussion\n\nDiscussed JWT authentication: token structure (header.payload.signature), verifyToken at src/auth/jwt.ts:42, refresh endpoint at src/routes/auth.ts:78. Security: httpOnly cookies, CSRF at src/middleware/csrf.ts:15, rate limiting at src/middleware/ratelimit.ts:30." + }, + { + "respond": "nudge-compress", + "growthText": "Frontend integration with the authentication backend. The React application uses a custom useAuth hook at src/hooks/useAuth.ts line 15 that manages authentication state including the current user loading state and error state. The hook communicates with the backend API using fetch with credentials include to send cookies. Protected routes are wrapped in an AuthGuard component at src/components/AuthGuard.tsx line 8 that redirects unauthenticated users to the login page. The login form at src/components/LoginForm.tsx line 20 handles form submission validation and error display. Token refresh is handled transparently by a fetch interceptor at src/lib/api.ts line 30 that catches 401 responses and automatically attempts a token refresh before retrying the original request. The interceptor uses a mutex to prevent multiple concurrent refresh attempts.", + "topic": "Auth System", + "summary": "## Auth System Discussion\n\nDiscussed JWT authentication: token structure (header.payload.signature), verifyToken at src/auth/jwt.ts:42, refresh endpoint at src/routes/auth.ts:78. Security: httpOnly cookies, CSRF at src/middleware/csrf.ts:15, rate limiting at src/middleware/ratelimit.ts:30." + }, + { + "respond": "nudge-compress", + "growthText": "Testing strategy for the authentication system. Unit tests at tests/auth.test.ts cover token generation verification and expiration logic using Jest. Integration tests at tests/integration/auth-flow.test.ts test the full authentication flow including login refresh and logout using a test database. E2E tests at tests/e2e/auth.spec.ts use Playwright to test the browser-based login flow including form validation error handling and redirect behavior. Mock implementations at src/mocks/handlers.ts line 10 provide mock API responses for development and testing. Test fixtures at tests/fixtures/users.ts provide test user data with different roles and permission levels. Code coverage target is 90 percent for authentication-related modules. Mutation testing using Stryker validates test quality by introducing deliberate defects.", + "topic": "Auth System", + "summary": "## Auth System Discussion\n\nDiscussed JWT authentication: token structure (header.payload.signature), verifyToken at src/auth/jwt.ts:42, refresh endpoint at src/routes/auth.ts:78. Security: httpOnly cookies, CSRF at src/middleware/csrf.ts:15, rate limiting at src/middleware/ratelimit.ts:30." + } + ], + "verify": { + "minBlockCount": 1, + "nudgeBaselineSet": true, + "maxCompressedCount": 10 + } +} diff --git a/scripts/e2e/verify.ts b/scripts/e2e/verify.ts index 3de52c5e..f4f957c2 100644 --- a/scripts/e2e/verify.ts +++ b/scripts/e2e/verify.ts @@ -9,6 +9,9 @@ interface VerifyExpectations { summaryContains?: string childBlockCount?: number nudgeBaselineSet?: boolean + compressedCount?: number + minCompressedCount?: number + maxCompressedCount?: number } interface VerifyScenario { @@ -149,6 +152,36 @@ if (expect.nudgeBaselineSet !== undefined) { ) } +function getCompressedMessageIds(s: any): string[] { + return Object.keys(s?.prune?.messages?.byMessageId ?? {}) +} + +const compressedIds = getCompressedMessageIds(state) + +if (expect.compressedCount !== undefined) { + assert( + `compressedCount === ${expect.compressedCount}`, + compressedIds.length === expect.compressedCount, + `got ${compressedIds.length} compressed message IDs`, + ) +} + +if (expect.minCompressedCount !== undefined) { + assert( + `compressedCount >= ${expect.minCompressedCount}`, + compressedIds.length >= expect.minCompressedCount, + `got ${compressedIds.length} compressed message IDs`, + ) +} + +if (expect.maxCompressedCount !== undefined) { + assert( + `compressedCount <= ${expect.maxCompressedCount}`, + compressedIds.length <= expect.maxCompressedCount, + `got ${compressedIds.length} compressed message IDs`, + ) +} + console.log() if (failed > 0) { console.error(`FAIL: ${failed} assertion(s) failed, ${passed} passed`)