From b121a2544b477e4ea98a55f6c7bfb2bb06decb45 Mon Sep 17 00:00:00 2001 From: insleker Date: Thu, 9 Apr 2026 11:26:35 +0800 Subject: [PATCH 1/2] chore: support windows OS --- docs/arch.md | 35 +++++- lib/src/cli_responder.dart | 5 +- lib/src/config.dart | 40 +++++- lib/src/issue_event_source/gitea_gosmee.dart | 5 +- lib/src/issue_event_source/github_gosmee.dart | 5 +- .../github_webhook_forward.dart | 3 +- lib/src/issue_tracker_client.dart | 4 +- lib/src/process_launcher.dart | 118 ++++++++++++++++++ pubspec.yaml | 10 +- skills-lock.json | 34 ++--- test/app_config_test.dart | 58 ++++++--- ...rrency_test.dart => concurrency_test.dart} | 0 ...run_once_core_test.dart => core_test.dart} | 0 .../github_gosmee_test.dart | 0 .../gosmee_test.dart | 22 ++-- .../webhook_forward_test.dart | 14 +-- ...ject_test.dart => multi_project_test.dart} | 0 ...ions_test.dart => notifications_test.dart} | 0 ...pp_run_retry_test.dart => retry_test.dart} | 0 test/test_support.dart | 96 ++++++++++---- ..._worktree_test.dart => worktree_test.dart} | 14 +-- 21 files changed, 368 insertions(+), 95 deletions(-) create mode 100644 lib/src/process_launcher.dart rename test/{issue_assistant_app_run_once_concurrency_test.dart => concurrency_test.dart} (100%) rename test/{issue_assistant_app_run_once_core_test.dart => core_test.dart} (100%) rename test/{issue_assistant_app_run => issue_event_source}/github_gosmee_test.dart (100%) rename test/{issue_assistant_app_run => issue_event_source}/gosmee_test.dart (95%) rename test/{issue_assistant_app_run => issue_event_source}/webhook_forward_test.dart (97%) rename test/{issue_assistant_app_run_once_multi_project_test.dart => multi_project_test.dart} (100%) rename test/{issue_assistant_app_run_once_notifications_test.dart => notifications_test.dart} (100%) rename test/{issue_assistant_app_run_retry_test.dart => retry_test.dart} (100%) rename test/{issue_assistant_app_run_once_worktree_test.dart => worktree_test.dart} (96%) diff --git a/docs/arch.md b/docs/arch.md index 6a1b0be..d8fcfeb 100644 --- a/docs/arch.md +++ b/docs/arch.md @@ -77,7 +77,8 @@ flowchart s2 s2 n3 --> s3 - s2 -->|"poll through API to execute job"| s4 + s2 + s4 s4 -->|"report execute status through API"| s2 n1@{ label: "Rectangle" } n1["runners dashboard"] @@ -90,4 +91,34 @@ flowchart s1["workflow dashboard"] end s1 --> s2 -``` \ No newline at end of file + n8["github polling"] + n9["github webhook + webhook-websocket gateway"] + s2 --> n8 + n8 --> s4 + s2 --> n9 + n9 --> s4 +``` + +## config option + +* (issue) tracker + * github(`gh`) + * polling + * `gh webhook forward` + * webhook + smee.io(`gosmee`) + * gitlab(`glab`) + * webhook + smee.io + * gitea(`tea`) + * webhook + smee.io + * openproject(`openproject-cli`) + * polling + * linear(`linear`) +* SCM + * github + * gitlab + * gitea +* notifier + * desktop + * discord + * slack + * email diff --git a/lib/src/cli_responder.dart b/lib/src/cli_responder.dart index 128fdf6..4ce889b 100644 --- a/lib/src/cli_responder.dart +++ b/lib/src/cli_responder.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'app_environment.dart'; import 'config.dart'; import 'issue_tracker_client.dart'; +import 'process_launcher.dart'; class CliResponderRunner { Future run({ @@ -27,12 +27,11 @@ class CliResponderRunner { }; final stdinPayload = renderTemplate(responder.stdinTemplate, context); - final process = await Process.start( + final process = await startExternalCommand( responder.command, responder.args, workingDirectory: workspacePath, environment: { - ...AppEnvironment.variables, ...responder.env, 'CWS_PROJECT_KEY': project.key, 'CWS_REPO': project.repo, diff --git a/lib/src/config.dart b/lib/src/config.dart index 8f08b61..2faf7e9 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -647,13 +647,51 @@ String _expandHome(String path) { return p.normalize(p.absolute(path)); } - final home = AppEnvironment.get('HOME'); + final home = _resolveHomeDirectory(); if (home == null || home.isEmpty) { throw const FileSystemException('HOME is not set.'); } return p.normalize(p.join(home, path.substring(2))); } +String? _resolveHomeDirectory() { + final home = _readEnvironmentValue('HOME'); + if (home != null && home.isNotEmpty) { + return home; + } + + final userProfile = _readEnvironmentValue('USERPROFILE'); + if (userProfile != null && userProfile.isNotEmpty) { + return userProfile; + } + + final homeDrive = _readEnvironmentValue('HOMEDRIVE'); + final homePath = _readEnvironmentValue('HOMEPATH'); + if (homeDrive != null && + homeDrive.isNotEmpty && + homePath != null && + homePath.isNotEmpty) { + return '$homeDrive$homePath'; + } + + return null; +} + +String? _readEnvironmentValue(String key) { + final direct = AppEnvironment.get(key); + if (direct != null && direct.isNotEmpty) { + return direct; + } + + for (final entry in AppEnvironment.variables.entries) { + if (entry.key.toLowerCase() == key.toLowerCase() && + entry.value.isNotEmpty) { + return entry.value; + } + } + return null; +} + String? _expandOptionalPath(String? path) { if (path == null || path.isEmpty) { return null; diff --git a/lib/src/issue_event_source/gitea_gosmee.dart b/lib/src/issue_event_source/gitea_gosmee.dart index e2bacc4..4aaaacd 100644 --- a/lib/src/issue_event_source/gitea_gosmee.dart +++ b/lib/src/issue_event_source/gitea_gosmee.dart @@ -7,6 +7,7 @@ import 'package:github/github.dart'; import '../app_logger.dart'; import '../config.dart'; import '../issue_tracker_client.dart'; +import '../process_launcher.dart'; import 'base.dart'; class GiteaGosmeeIssueEventSource implements IssueEventSource { @@ -107,7 +108,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource { 'gitea/gosmee project=${project.key} start_client ' 'command="$gosmeeCommand client $publicUrl $localUrl"', ); - final process = await Process.start(gosmeeCommand, [ + final process = await startExternalCommand(gosmeeCommand, [ 'client', publicUrl, localUrl, @@ -284,7 +285,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource { } Future _createGosmeeChannelUrl(String localUrl) async { - final result = await Process.run(gosmeeCommand, [ + final result = await runExternalCommand(gosmeeCommand, [ '--output', 'json', 'client', diff --git a/lib/src/issue_event_source/github_gosmee.dart b/lib/src/issue_event_source/github_gosmee.dart index 1249764..214398f 100644 --- a/lib/src/issue_event_source/github_gosmee.dart +++ b/lib/src/issue_event_source/github_gosmee.dart @@ -7,6 +7,7 @@ import 'package:github/github.dart'; import '../app_logger.dart'; import '../config.dart'; import '../issue_tracker_client.dart'; +import '../process_launcher.dart'; import 'base.dart'; class GitHubGosmeeIssueEventSource implements IssueEventSource { @@ -107,7 +108,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource { 'github/gosmee project=${project.key} start_client ' 'command="$gosmeeCommand client $publicUrl $localUrl"', ); - final process = await Process.start(gosmeeCommand, [ + final process = await startExternalCommand(gosmeeCommand, [ 'client', publicUrl, localUrl, @@ -284,7 +285,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource { } Future _createGosmeeChannelUrl(String localUrl) async { - final result = await Process.run(gosmeeCommand, [ + final result = await runExternalCommand(gosmeeCommand, [ '--output', 'json', 'client', diff --git a/lib/src/issue_event_source/github_webhook_forward.dart b/lib/src/issue_event_source/github_webhook_forward.dart index 7d5aed7..2e13f9a 100644 --- a/lib/src/issue_event_source/github_webhook_forward.dart +++ b/lib/src/issue_event_source/github_webhook_forward.dart @@ -5,6 +5,7 @@ import 'dart:io'; import '../app_logger.dart'; import '../config.dart'; import '../issue_tracker_client.dart'; +import '../process_launcher.dart'; import 'base.dart'; class GitHubWebhookForwardIssueEventSource implements IssueEventSource { @@ -80,7 +81,7 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource { 'github/webhook-forward project=${project.key} start_forwarder ' 'command="${issueTrackerClient.ghCommand} webhook forward --events=issues,issue_comment --repo=${project.repo} --url=$url"', ); - final process = await Process.start(issueTrackerClient.ghCommand, [ + final process = await startExternalCommand(issueTrackerClient.ghCommand, [ 'webhook', 'forward', '--events=issues,issue_comment', diff --git a/lib/src/issue_tracker_client.dart b/lib/src/issue_tracker_client.dart index a5403e6..83f91f3 100644 --- a/lib/src/issue_tracker_client.dart +++ b/lib/src/issue_tracker_client.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:github/github.dart'; +import 'process_launcher.dart'; + import 'config_schema.dart'; class IssueTrackerClient { @@ -319,7 +321,7 @@ class IssueTrackerClient { } Future _runJson(String command, List arguments) async { - final result = await Process.run(command, arguments); + final result = await runExternalCommand(command, arguments); if (result.exitCode != 0) { throw ProcessException( command, diff --git a/lib/src/process_launcher.dart b/lib/src/process_launcher.dart new file mode 100644 index 0000000..2b32daa --- /dev/null +++ b/lib/src/process_launcher.dart @@ -0,0 +1,118 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class _ResolvedCommand { + _ResolvedCommand({ + required this.executable, + required this.arguments, + required this.usesBashShim, + }); + + final String executable; + final List arguments; + final bool usesBashShim; +} + +_ResolvedCommand _resolveCommand(String executable, List arguments) { + final extension = p.extension(executable).toLowerCase(); + final shouldUseBash = extension.isEmpty || extension == '.sh'; + + if (Platform.isWindows && shouldUseBash && File(executable).existsSync()) { + final bashScriptPath = _toBashPath(executable); + return _ResolvedCommand( + executable: 'wsl.exe', + arguments: ['--exec', 'bash', bashScriptPath, ...arguments], + usesBashShim: true, + ); + } + + return _ResolvedCommand( + executable: executable, + arguments: arguments, + usesBashShim: false, + ); +} + +String _toBashPath(String windowsPath) { + final normalized = windowsPath.replaceAll('\\', '/'); + final drivePrefixMatch = RegExp(r'^([A-Za-z]):/(.*)$').firstMatch(normalized); + if (drivePrefixMatch == null) { + return normalized; + } + + final driveLetter = drivePrefixMatch.group(1)!.toLowerCase(); + final rest = drivePrefixMatch.group(2)!; + return '/mnt/$driveLetter/$rest'; +} + +Future runExternalCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, +}) { + final resolved = _resolveCommand(executable, arguments); + final effectiveEnvironment = _effectiveEnvironmentForResolvedCommand( + environment: environment, + includeParentEnvironment: includeParentEnvironment, + usesBashShim: resolved.usesBashShim, + ); + return Process.run( + resolved.executable, + resolved.arguments, + workingDirectory: workingDirectory, + environment: effectiveEnvironment, + includeParentEnvironment: includeParentEnvironment, + ); +} + +Future startExternalCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal, +}) { + final resolved = _resolveCommand(executable, arguments); + final effectiveEnvironment = _effectiveEnvironmentForResolvedCommand( + environment: environment, + includeParentEnvironment: includeParentEnvironment, + usesBashShim: resolved.usesBashShim, + ); + return Process.start( + resolved.executable, + resolved.arguments, + workingDirectory: workingDirectory, + environment: effectiveEnvironment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + mode: mode, + ); +} + +Map? _effectiveEnvironmentForResolvedCommand({ + required Map? environment, + required bool includeParentEnvironment, + required bool usesBashShim, +}) { + if (!usesBashShim || environment == null || environment.isEmpty) { + return environment; + } + + final effectiveEnvironment = Map.from(environment); + final existingWslenvFromProcess = includeParentEnvironment + ? Platform.environment['WSLENV'] + : null; + final existingWslenv = + effectiveEnvironment['WSLENV'] ?? existingWslenvFromProcess; + final forwardedKeys = { + ...?existingWslenv?.split(':').where((entry) => entry.isNotEmpty), + ...effectiveEnvironment.keys.where((key) => key != 'WSLENV'), + }; + effectiveEnvironment['WSLENV'] = forwardedKeys.join(':'); + return effectiveEnvironment; +} diff --git a/pubspec.yaml b/pubspec.yaml index 2fd05f9..b64e2c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.0 # repository: https://github.com/my_org/my_repo environment: - sdk: ^3.11.4 + sdk: ^3.11.3 dependencies: args: ^2.7.0 @@ -30,3 +30,11 @@ dev_dependencies: json_serializable: ">=6.11.1 <6.12.0" lints: ^6.0.0 test: ^1.25.6 + +platforms: + # android: + # ios: + linux: + macos: + # web: + windows: diff --git a/skills-lock.json b/skills-lock.json index f508346..364e031 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -194,77 +194,77 @@ "dart-api-design": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "d60beeedc91e1be7c51d7fea38acb636c0519b8140159402afcf69f31846cfba" + "computedHash": "00f2a541ca05387967b6e44afbce8b3e235a45225e06a376ec98e91116b76c09" }, "dart-async-programming": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "ebff987d2ce82a0c0eebab1b7ebc724d29bb925c56273ee55775550d634a3c72" + "computedHash": "52f989f2cb1121b8c5122d4cf6c9dbe7283523e7144e67e1f86640d33a82ce28" }, "dart-code-generation": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "259e371481a822984e666cc5214c97ac1581a0adbbdb4d7cab3997c9b1de2009" + "computedHash": "815a84815bbe6681129a6d883278fa6d251d3f4e5a6509e8e1ca74cab4422279" }, "dart-compilation-deployment": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "862ea7fd5efb9bd9760d9eab58880a1f08bb9b64125f171b778ba6918f79980f" + "computedHash": "b327d665fd5514cd0e1a193c8c51745405d7476a5b8419c5d9228324b6afa7d9" }, "dart-concurrency-isolates": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "829a819bb37342813eca318e0008d93db83aa313daa58be1b9a508d5b1674a9f" + "computedHash": "a3e7ff69a58c462d3ea94d7092d1d03e21eeb43bdbb21174400a111da4a96721" }, "dart-documentation": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "0016010518730eb2aeaabbf5046667deb724040be855a5892db4a9735608ca75" + "computedHash": "5eb2845edf402683555208ede425b217484fc1fe21f63131fefe9b249f97697a" }, "dart-effective-style": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "ceaa8cbe82090bf759d763cdc83aa80df79ac2461d0d5f223f63e72f60901ae4" + "computedHash": "3b11a0ad963151c12bbf0302cd526dcfdfa6659e194455f168d8441fae60e895" }, "dart-idiomatic-usage": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "ac383cc321ce8eeac6412eecc466e68adfcb131fd98c5b74af9d4b33c64381bf" + "computedHash": "c45023b434e63316dd209afc094f7412fd99683869b917f140918d0c065406c8" }, "dart-language-syntax": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "d1b1f4027530bb736247f62135845c06f69148cd3464fc235f942324499aee54" + "computedHash": "457294ca1c8e21c0ceb643ded8dfa1a2df1a542bea5825c95d331b99d119bd25" }, "dart-migration-versioning": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "c3fefb316b55d7794a49bc226cee17a2fafc00d75621e23aee1dd3f8c15def3a" + "computedHash": "f94fb07b64c2a4f1922504bf4de531de927e076a74b3cbd291a75763c3a7d6cb" }, "dart-native-interop-ffi": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "cac814c48f43b964471b41e99622b471203bb2900845bcc03eca02b2a3162ed5" + "computedHash": "0e29e6b5e8cb803d7644c6f13340cc1e3265d23f068be4da0f0781954737b3c6" }, "dart-package-management": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "a1fbb5422c3d9bf980a310b55f8cb938b969234eda60a27038f65833e5d0e805" + "computedHash": "569d2a0bcb4b61a15fb8e0f3406d9c54ddc9e1caf44c92302047b91730bd9320" }, "dart-static-analysis": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "f13d7c751ed2d026da741fe7b2535fb11a8d045c48ee91b48c0bf7b8147a3758" + "computedHash": "6258439460032ed8204d18abb0f09112ad7969d2497221693b302ce800f12106" }, "dart-testing": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "c6857b34f27d704eadbb770e40512693e18e04bab7345c23818386a6996dea6a" + "computedHash": "2d8a5d92cfa90dd5ad13f8f38606bdcb6edec8fc344fde02f51475a29f05d87a" }, "dart-web-development": { "source": "dart-lang/skills", "sourceType": "github", - "computedHash": "2adc4b77a7d30a2c9c52fc96368df10a8242512c95c8bf2b3fc06abf208dc421" + "computedHash": "db064015bed95259c6f60ba6eb5123a97d6341e68a0925a60a3d57a31bf71d6c" }, "datanalysis-credit-risk": { "source": "github/awesome-copilot", @@ -839,7 +839,7 @@ "supabase-postgres-best-practices": { "source": "supabase/agent-skills", "sourceType": "github", - "computedHash": "9c87c315aed143ee3b34bec8117100f5035e0df09e6b23e1ecc772cff434c9ad" + "computedHash": "f4cd7570115d97af6bcb2946eb891a958c8d04a43b479871c9ccadbc670ad058" }, "technology-stack-blueprint-generator": { "source": "github/awesome-copilot", @@ -944,7 +944,7 @@ "worktrunk": { "source": "max-sixty/worktrunk", "sourceType": "github", - "computedHash": "12ed42480bc641bb017955e0884716ccf9d04389e744edad6dfcac4b01c60cca" + "computedHash": "d480468e16c7de39a121e6e760788357bb178495f8912b05f05f55977468d72e" }, "write-coding-standards-from-file": { "source": "github/awesome-copilot", diff --git a/test/app_config_test.dart b/test/app_config_test.dart index 107bfea..2cb00d2 100644 --- a/test/app_config_test.dart +++ b/test/app_config_test.dart @@ -5,6 +5,40 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; +String _resolveTestHomeDirectory() { + String? getEnv(String key) { + final direct = Platform.environment[key]; + if (direct != null && direct.isNotEmpty) { + return direct; + } + for (final entry in Platform.environment.entries) { + if (entry.key.toLowerCase() == key.toLowerCase() && + entry.value.isNotEmpty) { + return entry.value; + } + } + return null; + } + + final home = getEnv('HOME'); + if (home != null) { + return home; + } + + final userProfile = getEnv('USERPROFILE'); + if (userProfile != null) { + return userProfile; + } + + final homeDrive = getEnv('HOMEDRIVE'); + final homePath = getEnv('HOMEPATH'); + if (homeDrive != null && homePath != null) { + return '$homeDrive$homePath'; + } + + throw StateError('No home directory environment variables are set.'); +} + void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -358,16 +392,11 @@ projects: // When loading the app config from disk. final config = await AppConfig.load(configFile.path); + final home = _resolveTestHomeDirectory(); // Then the app exposes the configured data and worktree directories. - expect( - config.dataDir, - p.join(Platform.environment['HOME']!, '.agent-orchestrator'), - ); - expect( - config.worktreeDir, - p.join(Platform.environment['HOME']!, '.worktrees'), - ); + expect(config.dataDir, p.join(home, '.agent-orchestrator')); + expect(config.worktreeDir, p.join(home, '.worktrees')); // And each project keeps the configured camelCase settings. expect(config.projects['sample'], isNotNull); @@ -809,24 +838,19 @@ projects: // When writing the generated starter config to disk. await configFile.writeAsString(contents); final config = await AppConfig.load(configFile.path); + final home = _resolveTestHomeDirectory(); // Then the file parses through the normal app config loader. expect(config.projects['sample'], isNotNull); // And the required starter values stay available to users. - expect( - config.dataDir, - p.join(Platform.environment['HOME']!, '.agent-orchestrator'), - ); + expect(config.dataDir, p.join(home, '.agent-orchestrator')); expect(config.projects['sample']!.provider, IssueTrackerProvider.github); - expect( - config.worktreeDir, - p.join(Platform.environment['HOME']!, '.worktrees'), - ); + expect(config.worktreeDir, p.join(home, '.worktrees')); expect(config.projects['sample']!.repo, 'owner/repo'); expect( config.projects['sample']!.path, - p.join(Platform.environment['HOME']!, 'path/to/repo'), + p.join(home, 'path', 'to', 'repo'), ); expect(config.projects['sample']!.defaultBranch, 'main'); expect(config.projects['sample']!.issueAssistant.mentionTriggers, [ diff --git a/test/issue_assistant_app_run_once_concurrency_test.dart b/test/concurrency_test.dart similarity index 100% rename from test/issue_assistant_app_run_once_concurrency_test.dart rename to test/concurrency_test.dart diff --git a/test/issue_assistant_app_run_once_core_test.dart b/test/core_test.dart similarity index 100% rename from test/issue_assistant_app_run_once_core_test.dart rename to test/core_test.dart diff --git a/test/issue_assistant_app_run/github_gosmee_test.dart b/test/issue_event_source/github_gosmee_test.dart similarity index 100% rename from test/issue_assistant_app_run/github_gosmee_test.dart rename to test/issue_event_source/github_gosmee_test.dart diff --git a/test/issue_assistant_app_run/gosmee_test.dart b/test/issue_event_source/gosmee_test.dart similarity index 95% rename from test/issue_assistant_app_run/gosmee_test.dart rename to test/issue_event_source/gosmee_test.dart index 7a2d4fe..c2647ea 100644 --- a/test/issue_assistant_app_run/gosmee_test.dart +++ b/test/issue_event_source/gosmee_test.dart @@ -76,7 +76,7 @@ void registerIssueAssistantAppRunGosmeeTests() { final postedBody = File(p.join(sandbox.path, 'posted-body.md')); await teaScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -printf '%s\n' "\$*" >> "${teaLog.path}" + printf '%s\n' "\$*" >> "${bashScriptPath(teaLog.path)}" if [[ "\$1" == "api" && "\$2" == "user" ]]; then cat <<'JSON' @@ -102,7 +102,7 @@ fi if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/12/comments" ]]; then for arg in "\$@"; do if [[ "\$arg" == body=* ]]; then - printf '%s' "\${arg#body=}" > "${postedBody.path}" + printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}" fi done cat <<'JSON' @@ -125,7 +125,7 @@ fi echo "unexpected tea args: \$*" >&2 exit 1 '''); - await Process.run('chmod', ['+x', teaScript.path]); + await makeScriptExecutable(teaScript); // And a responder that emits a planning reply. final responderScript = File(p.join(sandbox.path, 'responder.sh')); @@ -226,7 +226,7 @@ projects: final gosmeeLog = File(p.join(sandbox.path, 'gosmee-log.jsonl')); await gosmeeScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -printf '%s\n' "\$*" >> "${gosmeeLog.path}" + printf '%s\n' "\$*" >> "${bashScriptPath(gosmeeLog.path)}" if [[ "\$1" == "--output" && "\$2" == "json" && "\$3" == "client" && "\$4" == "--new-url" ]]; then printf '%s\n' 'https://gosmee.example.test/reconcile' @@ -242,7 +242,7 @@ fi echo "unexpected gosmee args: \$*" >&2 exit 1 '''); - await Process.run('chmod', ['+x', gosmeeScript.path]); + await makeScriptExecutable(gosmeeScript); // And a tea script that returns a new issue only on a later reconciliation poll. final teaScript = File(p.join(sandbox.path, 'tea')); @@ -272,7 +272,7 @@ exit 1 ]; await teaScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -printf '%s\n' "\$*" >> "${teaLog.path}" + printf '%s\n' "\$*" >> "${bashScriptPath(teaLog.path)}" if [[ "\$1" == "api" && "\$2" == "user" ]]; then cat <<'JSON' @@ -283,11 +283,11 @@ fi if [[ "\$1" == "api" && "\$4" == repos/owner/sample/issues\\?state=open* ]]; then count=0 - if [[ -f "${issueListCount.path}" ]]; then - count="\$(cat "${issueListCount.path}")" + if [[ -f "${bashScriptPath(issueListCount.path)}" ]]; then + count="\$(cat "${bashScriptPath(issueListCount.path)}")" fi count=\$((count + 1)) - printf '%s' "\$count" > "${issueListCount.path}" + printf '%s' "\$count" > "${bashScriptPath(issueListCount.path)}" if [[ "\$count" -eq 1 ]]; then cat <<'JSON' [] @@ -310,7 +310,7 @@ fi if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then for arg in "\$@"; do if [[ "\$arg" == body=* ]]; then - printf '%s' "\${arg#body=}" > "${postedBody.path}" + printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}" fi done cat <<'JSON' @@ -333,7 +333,7 @@ fi echo "unexpected tea args: \$*" >&2 exit 1 '''); - await Process.run('chmod', ['+x', teaScript.path]); + await makeScriptExecutable(teaScript); // And a responder that emits a planning reply. final responderScript = File(p.join(sandbox.path, 'responder.sh')); diff --git a/test/issue_assistant_app_run/webhook_forward_test.dart b/test/issue_event_source/webhook_forward_test.dart similarity index 97% rename from test/issue_assistant_app_run/webhook_forward_test.dart rename to test/issue_event_source/webhook_forward_test.dart index 5af5825..9871183 100644 --- a/test/issue_assistant_app_run/webhook_forward_test.dart +++ b/test/issue_event_source/webhook_forward_test.dart @@ -178,7 +178,7 @@ fi echo "unexpected gh args: \$*" >&2 exit 1 '''); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); // And an orchestrator-style config that uses gh/webhook-forward. final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml')); @@ -284,7 +284,7 @@ projects: ]; await ghScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -printf '%s\n' "\$*" >> "${ghLog.path}" + printf '%s\n' "\$*" >> "${bashScriptPath(ghLog.path)}" if [[ "\$1" == "api" && "\$2" == "user" ]]; then cat <<'JSON' @@ -295,11 +295,11 @@ fi if [[ "\$1" == "api" && "\$4" == "search/issues" ]]; then count=0 - if [[ -f "${searchCountFile.path}" ]]; then - count="\$(cat "${searchCountFile.path}")" + if [[ -f "${bashScriptPath(searchCountFile.path)}" ]]; then + count="\$(cat "${bashScriptPath(searchCountFile.path)}")" fi count=\$((count + 1)) - printf '%s' "\$count" > "${searchCountFile.path}" + printf '%s' "\$count" > "${bashScriptPath(searchCountFile.path)}" if [[ "\$count" -eq 1 ]]; then cat <<'JSON' {"total_count":0,"incomplete_results":false,"items":[]} @@ -326,7 +326,7 @@ fi if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then for arg in "\$@"; do if [[ "\$arg" == body=* ]]; then - printf '%s' "\${arg#body=}" > "${postedBody.path}" + printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}" fi done cat <<'JSON' @@ -344,7 +344,7 @@ fi echo "unexpected gh args: \$*" >&2 exit 1 '''); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); // And a responder that emits a planning reply. final responderScript = File(p.join(sandbox.path, 'responder.sh')); diff --git a/test/issue_assistant_app_run_once_multi_project_test.dart b/test/multi_project_test.dart similarity index 100% rename from test/issue_assistant_app_run_once_multi_project_test.dart rename to test/multi_project_test.dart diff --git a/test/issue_assistant_app_run_once_notifications_test.dart b/test/notifications_test.dart similarity index 100% rename from test/issue_assistant_app_run_once_notifications_test.dart rename to test/notifications_test.dart diff --git a/test/issue_assistant_app_run_retry_test.dart b/test/retry_test.dart similarity index 100% rename from test/issue_assistant_app_run_retry_test.dart rename to test/retry_test.dart diff --git a/test/test_support.dart b/test/test_support.dart index a569a37..f166fe6 100644 --- a/test/test_support.dart +++ b/test/test_support.dart @@ -4,6 +4,54 @@ import 'dart:io'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; +Future makeScriptExecutable(File script) async { + if (Platform.isWindows) { + return; + } + + final result = await Process.run('chmod', ['+x', script.path]); + if (result.exitCode != 0) { + throw ProcessException( + 'chmod', + ['+x', script.path], + '${result.stdout}\n${result.stderr}', + result.exitCode, + ); + } +} + +String bashScriptPath(String hostPath) { + if (!Platform.isWindows) { + return hostPath; + } + + final normalized = hostPath.replaceAll('\\', '/'); + final drivePrefixMatch = RegExp(r'^([A-Za-z]):/(.*)$').firstMatch(normalized); + if (drivePrefixMatch == null) { + return normalized; + } + + final driveLetter = drivePrefixMatch.group(1)!.toLowerCase(); + final rest = drivePrefixMatch.group(2)!; + return '/mnt/$driveLetter/$rest'; +} + +String hostPathFromScript(String scriptPath) { + if (!Platform.isWindows) { + return scriptPath; + } + + final trimmed = scriptPath.trim(); + final mntPathMatch = RegExp(r'^/mnt/([a-zA-Z])/(.*)$').firstMatch(trimmed); + if (mntPathMatch == null) { + return trimmed; + } + + final driveLetter = mntPathMatch.group(1)!.toUpperCase(); + final rest = mntPathMatch.group(2)!.replaceAll('/', '\\'); + return '$driveLetter:\\$rest'; +} + Future writeFakeGhScript({ required File ghScript, required List> issueListResponse, @@ -71,7 +119,7 @@ Future writeFakeGhScript({ if (ghLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${ghLog.path}"', + '"${bashScriptPath(ghLog.path)}"', ); } @@ -154,7 +202,7 @@ Future writeFakeGhScript({ ..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln( r''' printf '%s' "${arg#body=}" > ''' - '"${postedBodyFile.path}"', + '"${bashScriptPath(postedBodyFile.path)}"', ) ..writeln(r''' fi'''); } @@ -179,7 +227,7 @@ Future writeFakeGhScript({ ..writeln('exit 1'); await ghScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); } Future writeFlakySearchGhScript({ @@ -210,7 +258,7 @@ Future writeFlakySearchGhScript({ if (ghLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${ghLog.path}"', + '"${bashScriptPath(ghLog.path)}"', ); } @@ -224,8 +272,10 @@ Future writeFlakySearchGhScript({ buffer ..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''') - ..writeln(' if [[ ! -f "${failureStateFile.path}" ]]; then') - ..writeln(' touch "${failureStateFile.path}"') + ..writeln( + ' if [[ ! -f "${bashScriptPath(failureStateFile.path)}" ]]; then', + ) + ..writeln(' touch "${bashScriptPath(failureStateFile.path)}"') ..writeln(r''' echo "error connecting to api.github.com" >&2''') ..writeln( r''' echo "check your internet connection or https://githubstatus.com" >&2''', @@ -256,7 +306,7 @@ Future writeFlakySearchGhScript({ ..writeln('exit 1'); await ghScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); } Future writeFakeTeaScript({ @@ -287,7 +337,7 @@ Future writeFakeTeaScript({ if (teaLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${teaLog.path}"', + '"${bashScriptPath(teaLog.path)}"', ); } @@ -352,7 +402,7 @@ Future writeFakeTeaScript({ ..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln( r''' printf '%s' "${arg#body=}" > ''' - '"${postedBodyFile.path}"', + '"${bashScriptPath(postedBodyFile.path)}"', ) ..writeln(r''' fi'''); } @@ -377,7 +427,7 @@ Future writeFakeTeaScript({ ..writeln('exit 1'); await teaScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', teaScript.path]); + await makeScriptExecutable(teaScript); } Future writeResponderScript( @@ -395,7 +445,7 @@ ${jsonEncode(response)} JSON exit $exitCode '''); - await Process.run('chmod', ['+x', responderScript.path]); + await makeScriptExecutable(responderScript); } Future writeJsonlResponderScript( @@ -412,7 +462,7 @@ cat <<'JSON' {"type":"turn.completed"} JSON '''); - await Process.run('chmod', ['+x', responderScript.path]); + await makeScriptExecutable(responderScript); } Future writeDelayedLoggingResponderScript( @@ -429,15 +479,15 @@ import time print(int(time.time() * 1000)) PY } -printf 'start:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${eventLog.path}" +printf 'start:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}" cat >/dev/null sleep ${delay.inSeconds} -printf 'end:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${eventLog.path}" +printf 'end:%s:%s\n' "\$CWS_ISSUE_NUMBER" "\$(timestamp_ms)" >> "${bashScriptPath(eventLog.path)}" cat <<'JSON' ${jsonEncode(response)} JSON '''); - await Process.run('chmod', ['+x', responderScript.path]); + await makeScriptExecutable(responderScript); } Future writeWebhookForwardGhScript({ @@ -478,7 +528,7 @@ Future writeWebhookForwardGhScript({ if (ghLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${ghLog.path}"', + '"${bashScriptPath(ghLog.path)}"', ); } @@ -521,7 +571,7 @@ Future writeWebhookForwardGhScript({ ..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln( r''' printf '%s' "${arg#body=}" > ''' - '"${postedBodyFile.path}"', + '"${bashScriptPath(postedBodyFile.path)}"', ) ..writeln(r''' fi'''); } @@ -567,7 +617,7 @@ Future writeWebhookForwardGhScript({ ..writeln('exit 1'); await ghScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); } Future writeGitHubGosmeeGhScript({ @@ -601,7 +651,7 @@ Future writeGitHubGosmeeGhScript({ if (ghLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${ghLog.path}"', + '"${bashScriptPath(ghLog.path)}"', ); } @@ -668,7 +718,7 @@ Future writeGitHubGosmeeGhScript({ ..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln( r''' printf '%s' "${arg#body=}" > ''' - '"${postedBodyFile.path}"', + '"${bashScriptPath(postedBodyFile.path)}"', ) ..writeln(r''' fi'''); } @@ -698,7 +748,7 @@ Future writeGitHubGosmeeGhScript({ ..writeln('exit 1'); await ghScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', ghScript.path]); + await makeScriptExecutable(ghScript); } Future writeGosmeeForwardScript({ @@ -714,7 +764,7 @@ Future writeGosmeeForwardScript({ if (gosmeeLog != null) { buffer.writeln( r'''printf '%s\n' "$*" >> ''' - '"${gosmeeLog.path}"', + '"${bashScriptPath(gosmeeLog.path)}"', ); } @@ -757,7 +807,7 @@ Future writeGosmeeForwardScript({ ..writeln('exit 1'); await gosmeeScript.writeAsString(buffer.toString()); - await Process.run('chmod', ['+x', gosmeeScript.path]); + await makeScriptExecutable(gosmeeScript); } Future initGitRepo(Directory directory) async { diff --git a/test/issue_assistant_app_run_once_worktree_test.dart b/test/worktree_test.dart similarity index 96% rename from test/issue_assistant_app_run_once_worktree_test.dart rename to test/worktree_test.dart index 9595a7b..5c83cab 100644 --- a/test/issue_assistant_app_run_once_worktree_test.dart +++ b/test/worktree_test.dart @@ -78,13 +78,13 @@ void registerIssueAssistantAppRunOnceWorktreeTests() { ); await responderScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -pwd > "${cwdCapture.path}" -cat > "${stdinCapture.path}" +pwd > "${bashScriptPath(cwdCapture.path)}" +cat > "${bashScriptPath(stdinCapture.path)}" cat <<'JSON' {"decision":"no_reply","mode":"planning","summary":"captured mention context"} JSON '''); - await Process.run('chmod', ['+x', responderScript.path]); + await makeScriptExecutable(responderScript); // And an orchestrator-style config pointing to the fake repo and responder. final configFile = File( @@ -115,7 +115,7 @@ projects: await app.close(); // Then the responder runs from an ephemeral worktree instead of the source checkout. - final capturedCwd = await cwdCapture.readAsString(); + final capturedCwd = hostPathFromScript(await cwdCapture.readAsString()); expect( p.normalize(capturedCwd.trim()), isNot(p.normalize(repoDir.path)), @@ -201,13 +201,13 @@ projects: ); await responderScript.writeAsString('''#!/usr/bin/env bash set -euo pipefail -pwd > "${cwdCapture.path}" +pwd > "${bashScriptPath(cwdCapture.path)}" cat >/dev/null cat <<'JSON' {"decision":"reply","mode":"planning","markdown":"planning reply","summary":"used project path"} JSON '''); - await Process.run('chmod', ['+x', responderScript.path]); + await makeScriptExecutable(responderScript); // And an orchestrator-style config pointing to the fake repo and responder. final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml')); @@ -237,7 +237,7 @@ projects: await app.close(); // Then the responder runs from an ephemeral worktree instead of the source checkout. - final capturedCwd = await cwdCapture.readAsString(); + final capturedCwd = hostPathFromScript(await cwdCapture.readAsString()); expect(p.normalize(capturedCwd.trim()), isNot(p.normalize(repoDir.path))); expect( p.isWithin(worktreeRoot.path, p.normalize(capturedCwd.trim())), From 2d5900b11832341918a52ea75336c32db1c03446 Mon Sep 17 00:00:00 2001 From: insleker Date: Thu, 9 Apr 2026 11:38:38 +0800 Subject: [PATCH 2/2] chore: adjust test files layout --- test/{ => config}/app_config_test.dart | 0 test/{ => config}/app_logger_test.dart | 0 test/{ => config}/init_config_cli_test.dart | 0 test/{ => core}/concurrency_test.dart | 2 +- test/{ => core}/core_test.dart | 2 +- test/{ => core}/multi_project_test.dart | 2 +- test/{ => core}/retry_test.dart | 2 +- .../routed_issue_event_source_test.dart | 0 test/{ => notifier}/notifications_test.dart | 2 +- test/{ => notifier}/notifier_test.dart | 0 test/{ => workspace}/workspace_manager_test.dart | 2 +- test/{ => workspace}/worktree_test.dart | 2 +- 12 files changed, 7 insertions(+), 7 deletions(-) rename test/{ => config}/app_config_test.dart (100%) rename test/{ => config}/app_logger_test.dart (100%) rename test/{ => config}/init_config_cli_test.dart (100%) rename test/{ => core}/concurrency_test.dart (99%) rename test/{ => core}/core_test.dart (99%) rename test/{ => core}/multi_project_test.dart (99%) rename test/{ => core}/retry_test.dart (99%) rename test/{ => issue_event_source}/routed_issue_event_source_test.dart (100%) rename test/{ => notifier}/notifications_test.dart (99%) rename test/{ => notifier}/notifier_test.dart (100%) rename test/{ => workspace}/workspace_manager_test.dart (98%) rename test/{ => workspace}/worktree_test.dart (99%) diff --git a/test/app_config_test.dart b/test/config/app_config_test.dart similarity index 100% rename from test/app_config_test.dart rename to test/config/app_config_test.dart diff --git a/test/app_logger_test.dart b/test/config/app_logger_test.dart similarity index 100% rename from test/app_logger_test.dart rename to test/config/app_logger_test.dart diff --git a/test/init_config_cli_test.dart b/test/config/init_config_cli_test.dart similarity index 100% rename from test/init_config_cli_test.dart rename to test/config/init_config_cli_test.dart diff --git a/test/concurrency_test.dart b/test/core/concurrency_test.dart similarity index 99% rename from test/concurrency_test.dart rename to test/core/concurrency_test.dart index 2962bb7..42995de 100644 --- a/test/concurrency_test.dart +++ b/test/core/concurrency_test.dart @@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunOnceConcurrencyTests() { /// ```gherkin diff --git a/test/core_test.dart b/test/core/core_test.dart similarity index 99% rename from test/core_test.dart rename to test/core/core_test.dart index 38dbccc..85d0abd 100644 --- a/test/core_test.dart +++ b/test/core/core_test.dart @@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunOnceCoreTests() { /// ```gherkin diff --git a/test/multi_project_test.dart b/test/core/multi_project_test.dart similarity index 99% rename from test/multi_project_test.dart rename to test/core/multi_project_test.dart index 168558a..166d186 100644 --- a/test/multi_project_test.dart +++ b/test/core/multi_project_test.dart @@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunOnceMultiProjectTests() { /// ```gherkin diff --git a/test/retry_test.dart b/test/core/retry_test.dart similarity index 99% rename from test/retry_test.dart rename to test/core/retry_test.dart index f6ed8e4..542a012 100644 --- a/test/retry_test.dart +++ b/test/core/retry_test.dart @@ -6,7 +6,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunRetryTests() { /// ```gherkin diff --git a/test/routed_issue_event_source_test.dart b/test/issue_event_source/routed_issue_event_source_test.dart similarity index 100% rename from test/routed_issue_event_source_test.dart rename to test/issue_event_source/routed_issue_event_source_test.dart diff --git a/test/notifications_test.dart b/test/notifier/notifications_test.dart similarity index 99% rename from test/notifications_test.dart rename to test/notifier/notifications_test.dart index 2dfa294..1999c11 100644 --- a/test/notifications_test.dart +++ b/test/notifier/notifications_test.dart @@ -7,7 +7,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunOnceNotificationTests() { /// ```gherkin diff --git a/test/notifier_test.dart b/test/notifier/notifier_test.dart similarity index 100% rename from test/notifier_test.dart rename to test/notifier/notifier_test.dart diff --git a/test/workspace_manager_test.dart b/test/workspace/workspace_manager_test.dart similarity index 98% rename from test/workspace_manager_test.dart rename to test/workspace/workspace_manager_test.dart index c89de68..ad384c8 100644 --- a/test/workspace_manager_test.dart +++ b/test/workspace/workspace_manager_test.dart @@ -4,7 +4,7 @@ import 'package:code_work_spawner/code_work_spawner.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void main() { /// ```gherkin diff --git a/test/worktree_test.dart b/test/workspace/worktree_test.dart similarity index 99% rename from test/worktree_test.dart rename to test/workspace/worktree_test.dart index 5c83cab..72729d5 100644 --- a/test/worktree_test.dart +++ b/test/workspace/worktree_test.dart @@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import 'test_support.dart'; +import '../test_support.dart'; void registerIssueAssistantAppRunOnceWorktreeTests() { /// ```gherkin