diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcc5cbeb..e0f1f099 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,12 @@ jobs: api/package-lock.json worker/package-lock.json + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - run: ./scripts/lint.sh build: @@ -62,6 +68,12 @@ jobs: api/package-lock.json worker/package-lock.json + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - run: ./scripts/test.sh release-scripts: diff --git a/.jules/forge.md b/.jules/forge.md index e5788bbd..53cfef03 100644 --- a/.jules/forge.md +++ b/.jules/forge.md @@ -6,3 +6,4 @@ - **[2026-06-15]**: Removed obsolete Python/`uv` tooling from `AGENTS.md` as the project is now fully Node.js. Identified a gap where `worker` build checks were missing from `.github/workflows/ci.yml`. Added a `worker` job to `ci.yml` to run `npm ci` and `npm run build`, ensuring type safety and parity with `AGENTS.md` intent. - **[2026-04-08]**: Updated `google-github-actions/auth` from v2 to v3 in `provision-worker.yml` to prevent deprecation issues. Extracted inline YAML scripts for Doppler secrets, Forge version resolution, worker notifications, and worker host config into reusable `scripts/` to enforce execution parity and Kickstart principles. Refined `AGENTS.md` to expose CI parity scripts (`./scripts/test.sh`, etc.) alongside targeted commands. - **[2026-04-11]**: Updated `google-github-actions/auth` from v2 to v3 in `rollout-safety-net.yml` to prevent deprecation issues. Extracted inline YAML scripts for rollout safety net polling, setting image names, checking docker tags, and extracting GCP project IDs into reusable `scripts/` to enforce execution parity and Kickstart verification principles. Refined `scripts/run-analysis.js` to mitigate command injection risks by removing `shell: true` from `child_process.spawn`. +- **[2026-06-10]**: Calibrated `ci.yml` and verification scripts (`scripts/lint.sh`, `scripts/test.sh`) to enforce CI execution parity for the `worker_flutter` desktop worker using `subosito/flutter-action@v2`. Added `worker_flutter` definitions to `AGENTS.md`. diff --git a/AGENTS.md b/AGENTS.md index fa0850de..13e9b6f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ - **Node.js**: 20+ (managed by `npm`) - **Docker**: Required for simulation engine - **Package Manager**: `npm` (Node) +- **Flutter**: Desktop worker (`worker_flutter`) ## Setup - **Install Node Dependencies**: `npm run install:all` @@ -29,6 +30,8 @@ - **Worker Install**: `cd worker && npm install` - **Worker Dev**: `cd worker && npm run dev` - **Worker Test**: `cd worker && npm run test:unit` +- **Worker Flutter Lint**: `cd worker_flutter && flutter pub get && flutter analyze --no-fatal-infos` +- **Worker Flutter Test**: `cd worker_flutter && flutter pub get && flutter test` ## Deployment - **CI**: `.github/workflows/ci.yml` (Runs on PR) @@ -38,3 +41,4 @@ - `frontend/`: React app - `api/`: Next.js API, ingestion, and analysis - `worker/`: Node.js + Docker simulation runner +- `worker_flutter/`: Flutter desktop worker diff --git a/scripts/lint.sh b/scripts/lint.sh index 528e316c..af4c5369 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -15,3 +15,7 @@ cd frontend && npm ci && npm run lint && cd .. # API echo "=== API lint ===" cd api && npm ci && npm run lint && cd .. + +# Worker Flutter +echo "=== Worker Flutter lint ===" +cd worker_flutter && flutter pub get && flutter analyze --no-fatal-infos && cd .. diff --git a/scripts/test.sh b/scripts/test.sh index 31b58634..41399ecc 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -22,3 +22,7 @@ npm run test:ingestion && cd .. # Worker tests echo "=== Worker unit tests ===" cd worker && npm ci && npm run test:unit && cd .. + +# Worker Flutter tests +echo "=== Worker Flutter tests ===" +cd worker_flutter && flutter pub get && flutter test && cd .. diff --git a/worker_flutter/lib/main.dart b/worker_flutter/lib/main.dart index af23c0da..289325c9 100644 --- a/worker_flutter/lib/main.dart +++ b/worker_flutter/lib/main.dart @@ -19,7 +19,6 @@ import 'installer/install_progress_app.dart'; import 'installer/installer.dart'; import 'launch/auto_start_service.dart'; import 'launch/mode_picker_screen.dart'; -import 'macos/activation_policy.dart'; import 'offline/offline_app.dart'; import 'sentry_setup.dart'; import 'telemetry.dart'; diff --git a/worker_flutter/lib/offline/db/app_db.dart b/worker_flutter/lib/offline/db/app_db.dart index aa736d60..284d50f5 100644 --- a/worker_flutter/lib/offline/db/app_db.dart +++ b/worker_flutter/lib/offline/db/app_db.dart @@ -79,7 +79,7 @@ class AppDb extends _$AppDb { /// test suite with `NativeDatabase.memory()` for hermetic, fast /// integration tests that exercise the same SQL the app does at /// runtime — but without touching disk. - AppDb.forTesting(QueryExecutor e) : super(e); + AppDb.forTesting(super.e); @override int get schemaVersion => 2; diff --git a/worker_flutter/lib/offline/deck_source.dart b/worker_flutter/lib/offline/deck_source.dart index 6f221bac..7231cf24 100644 --- a/worker_flutter/lib/offline/deck_source.dart +++ b/worker_flutter/lib/offline/deck_source.dart @@ -38,7 +38,7 @@ class PreconDeck { } /// Loads precons from the Flutter asset bundle PLUS any extra .dck -/// files the user may have dropped into `/res/Decks/ +/// files the user may have dropped into `<forgePath>/res/Decks/ /// Commander/`. The bundled set is the floor — offline mode works /// from a fresh install without a Forge download. Future> loadBundledPrecons(String forgePath) async { diff --git a/worker_flutter/lib/sims/deck_picker_section.dart b/worker_flutter/lib/sims/deck_picker_section.dart index 04246fd0..93b5bf73 100644 --- a/worker_flutter/lib/sims/deck_picker_section.dart +++ b/worker_flutter/lib/sims/deck_picker_section.dart @@ -40,12 +40,12 @@ class DeckPickerSection extends StatefulWidget { } class _DeckPickerSectionState extends State { - late final ExpansionTileController _ctrl; + late final ExpansibleController _ctrl; @override void initState() { super.initState(); - _ctrl = ExpansionTileController(); + _ctrl = ExpansibleController(); } @override diff --git a/worker_flutter/lib/telemetry.dart b/worker_flutter/lib/telemetry.dart index 146c2c48..14c65bd9 100644 --- a/worker_flutter/lib/telemetry.dart +++ b/worker_flutter/lib/telemetry.dart @@ -73,9 +73,9 @@ class Telemetry { tags.forEach(scope.setTag); } if (extra != null) { - // ignore: deprecated_member_use — `setExtra` is the documented + // ignore: deprecated_member_use — `setContexts` is the documented // 8.x API. Sentry's 9.x release moves to structured contexts. - extra.forEach(scope.setExtra); + scope.setContexts('extra', extra); } }, ); @@ -111,9 +111,10 @@ SentryEvent? scrubPii(SentryEvent event, {Hint? hint}) { ? event : event.copyWith(user: SentryUser(id: '[redacted]')); - // ignore: deprecated_member_use — see comment in Telemetry.captureError. - final extra = cleaned.extra; - if (extra != null) { + final contexts = cleaned.contexts.cast(); + final dynamic extraDynamic = contexts['extra']; + if (extraDynamic is Map) { + final extra = extraDynamic; for (final key in extra.keys.toList()) { if (_piiKeys.contains(key)) { extra[key] = '[redacted]'; diff --git a/worker_flutter/pubspec.lock b/worker_flutter/pubspec.lock index b7c9feee..d21e63a0 100644 --- a/worker_flutter/pubspec.lock +++ b/worker_flutter/pubspec.lock @@ -692,10 +692,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -1193,10 +1193,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" tray_manager: dependency: "direct main" description: @@ -1374,5 +1374,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.5 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.38.4" diff --git a/worker_flutter/pubspec.yaml b/worker_flutter/pubspec.yaml index 5fb626dc..ffa79515 100644 --- a/worker_flutter/pubspec.yaml +++ b/worker_flutter/pubspec.yaml @@ -10,7 +10,7 @@ publish_to: 'none' version: 0.2.0+1 environment: - sdk: ^3.11.5 + sdk: ^3.11.0 dependencies: flutter: diff --git a/worker_flutter/test/offline/resume_test.dart b/worker_flutter/test/offline/resume_test.dart index 7a57724e..ab976908 100644 --- a/worker_flutter/test/offline/resume_test.dart +++ b/worker_flutter/test/offline/resume_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:drift/drift.dart' show Value; diff --git a/worker_flutter/test/telemetry_test.dart b/worker_flutter/test/telemetry_test.dart index 8157e195..d052aaa5 100644 --- a/worker_flutter/test/telemetry_test.dart +++ b/worker_flutter/test/telemetry_test.dart @@ -17,40 +17,48 @@ void main() { }); group('scrubPii', () { - test('redacts email-shaped values in event extras', () { + test('redacts email-shaped values in event contexts', () { + final contexts = Contexts(); + contexts['extra'] = { + 'note': 'user contacted me at someone@example.com about it', + 'okField': 'no pii here', + }; final event = SentryEvent(message: const SentryMessage('hi')).copyWith( - extra: { - 'note': 'user contacted me at someone@example.com about it', - 'okField': 'no pii here', - }, + contexts: contexts, ); final scrubbed = scrubPii(event); expect(scrubbed, isNotNull); - expect(scrubbed!.extra!['note'], contains('[redacted-email]')); - expect(scrubbed.extra!['note'], isNot(contains('someone@example.com'))); - expect(scrubbed.extra!['okField'], 'no pii here'); + final extra = scrubbed!.contexts.cast()['extra'] + as Map; + expect(extra['note'], contains('[redacted-email]')); + expect(extra['note'], isNot(contains('someone@example.com'))); + expect(extra['okField'], 'no pii here'); }); test('redacts known PII keys', () { + final contexts = Contexts(); + contexts['extra'] = { + 'email': 'a@b.com', + 'uid': 'xyz', + 'displayName': 'Tyler', + 'user_id': '42', + 'safe': 'keep me', + }; final event = SentryEvent(message: const SentryMessage('hi')).copyWith( - extra: { - 'email': 'a@b.com', - 'uid': 'xyz', - 'displayName': 'Tyler', - 'user_id': '42', - 'safe': 'keep me', - }, + contexts: contexts, ); final scrubbed = scrubPii(event); - expect(scrubbed!.extra!['email'], '[redacted]'); - expect(scrubbed.extra!['uid'], '[redacted]'); - expect(scrubbed.extra!['displayName'], '[redacted]'); - expect(scrubbed.extra!['user_id'], '[redacted]'); - expect(scrubbed.extra!['safe'], 'keep me'); + final extra = scrubbed!.contexts.cast()['extra'] + as Map; + expect(extra['email'], '[redacted]'); + expect(extra['uid'], '[redacted]'); + expect(extra['displayName'], '[redacted]'); + expect(extra['user_id'], '[redacted]'); + expect(extra['safe'], 'keep me'); }); test('replaces user with redacted placeholder', () { diff --git a/worker_flutter/test/worker/log_uploader_test.dart b/worker_flutter/test/worker/log_uploader_test.dart index 1f584393..ad1208d9 100644 --- a/worker_flutter/test/worker/log_uploader_test.dart +++ b/worker_flutter/test/worker/log_uploader_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:convert'; import 'package:flutter_test/flutter_test.dart';