Merge pull request #29 from existedinnettw/refactor/layout

This commit is contained in:
existedinnettw 2026-04-09 11:40:55 +08:00 committed by GitHub
commit 1c2e7f5e49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 375 additions and 102 deletions

View File

@ -77,7 +77,8 @@ flowchart
s2 s2
s2 s2
n3 --> s3 n3 --> s3
s2 -->|"poll through API to execute job"| s4 s2
s4
s4 -->|"report execute status through API"| s2 s4 -->|"report execute status through API"| s2
n1@{ label: "Rectangle" } n1@{ label: "Rectangle" }
n1["runners dashboard"] n1["runners dashboard"]
@ -90,4 +91,34 @@ flowchart
s1["workflow dashboard"] s1["workflow dashboard"]
end end
s1 --> s2 s1 --> s2
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

View File

@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'app_environment.dart';
import 'config.dart'; import 'config.dart';
import 'issue_tracker_client.dart'; import 'issue_tracker_client.dart';
import 'process_launcher.dart';
class CliResponderRunner { class CliResponderRunner {
Future<ResponderResult> run({ Future<ResponderResult> run({
@ -27,12 +27,11 @@ class CliResponderRunner {
}; };
final stdinPayload = renderTemplate(responder.stdinTemplate, context); final stdinPayload = renderTemplate(responder.stdinTemplate, context);
final process = await Process.start( final process = await startExternalCommand(
responder.command, responder.command,
responder.args, responder.args,
workingDirectory: workspacePath, workingDirectory: workspacePath,
environment: <String, String>{ environment: <String, String>{
...AppEnvironment.variables,
...responder.env, ...responder.env,
'CWS_PROJECT_KEY': project.key, 'CWS_PROJECT_KEY': project.key,
'CWS_REPO': project.repo, 'CWS_REPO': project.repo,

View File

@ -647,13 +647,51 @@ String _expandHome(String path) {
return p.normalize(p.absolute(path)); return p.normalize(p.absolute(path));
} }
final home = AppEnvironment.get('HOME'); final home = _resolveHomeDirectory();
if (home == null || home.isEmpty) { if (home == null || home.isEmpty) {
throw const FileSystemException('HOME is not set.'); throw const FileSystemException('HOME is not set.');
} }
return p.normalize(p.join(home, path.substring(2))); 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) { String? _expandOptionalPath(String? path) {
if (path == null || path.isEmpty) { if (path == null || path.isEmpty) {
return null; return null;

View File

@ -7,6 +7,7 @@ import 'package:github/github.dart';
import '../app_logger.dart'; import '../app_logger.dart';
import '../config.dart'; import '../config.dart';
import '../issue_tracker_client.dart'; import '../issue_tracker_client.dart';
import '../process_launcher.dart';
import 'base.dart'; import 'base.dart';
class GiteaGosmeeIssueEventSource implements IssueEventSource { class GiteaGosmeeIssueEventSource implements IssueEventSource {
@ -107,7 +108,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
'gitea/gosmee project=${project.key} start_client ' 'gitea/gosmee project=${project.key} start_client '
'command="$gosmeeCommand client $publicUrl $localUrl"', 'command="$gosmeeCommand client $publicUrl $localUrl"',
); );
final process = await Process.start(gosmeeCommand, [ final process = await startExternalCommand(gosmeeCommand, [
'client', 'client',
publicUrl, publicUrl,
localUrl, localUrl,
@ -284,7 +285,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
} }
Future<String> _createGosmeeChannelUrl(String localUrl) async { Future<String> _createGosmeeChannelUrl(String localUrl) async {
final result = await Process.run(gosmeeCommand, [ final result = await runExternalCommand(gosmeeCommand, [
'--output', '--output',
'json', 'json',
'client', 'client',

View File

@ -7,6 +7,7 @@ import 'package:github/github.dart';
import '../app_logger.dart'; import '../app_logger.dart';
import '../config.dart'; import '../config.dart';
import '../issue_tracker_client.dart'; import '../issue_tracker_client.dart';
import '../process_launcher.dart';
import 'base.dart'; import 'base.dart';
class GitHubGosmeeIssueEventSource implements IssueEventSource { class GitHubGosmeeIssueEventSource implements IssueEventSource {
@ -107,7 +108,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
'github/gosmee project=${project.key} start_client ' 'github/gosmee project=${project.key} start_client '
'command="$gosmeeCommand client $publicUrl $localUrl"', 'command="$gosmeeCommand client $publicUrl $localUrl"',
); );
final process = await Process.start(gosmeeCommand, [ final process = await startExternalCommand(gosmeeCommand, [
'client', 'client',
publicUrl, publicUrl,
localUrl, localUrl,
@ -284,7 +285,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
} }
Future<String> _createGosmeeChannelUrl(String localUrl) async { Future<String> _createGosmeeChannelUrl(String localUrl) async {
final result = await Process.run(gosmeeCommand, [ final result = await runExternalCommand(gosmeeCommand, [
'--output', '--output',
'json', 'json',
'client', 'client',

View File

@ -5,6 +5,7 @@ import 'dart:io';
import '../app_logger.dart'; import '../app_logger.dart';
import '../config.dart'; import '../config.dart';
import '../issue_tracker_client.dart'; import '../issue_tracker_client.dart';
import '../process_launcher.dart';
import 'base.dart'; import 'base.dart';
class GitHubWebhookForwardIssueEventSource implements IssueEventSource { class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
@ -80,7 +81,7 @@ class GitHubWebhookForwardIssueEventSource implements IssueEventSource {
'github/webhook-forward project=${project.key} start_forwarder ' 'github/webhook-forward project=${project.key} start_forwarder '
'command="${issueTrackerClient.ghCommand} webhook forward --events=issues,issue_comment --repo=${project.repo} --url=$url"', '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', 'webhook',
'forward', 'forward',
'--events=issues,issue_comment', '--events=issues,issue_comment',

View File

@ -3,6 +3,8 @@ import 'dart:io';
import 'package:github/github.dart'; import 'package:github/github.dart';
import 'process_launcher.dart';
import 'config_schema.dart'; import 'config_schema.dart';
class IssueTrackerClient { class IssueTrackerClient {
@ -319,7 +321,7 @@ class IssueTrackerClient {
} }
Future<Object?> _runJson(String command, List<String> arguments) async { Future<Object?> _runJson(String command, List<String> arguments) async {
final result = await Process.run(command, arguments); final result = await runExternalCommand(command, arguments);
if (result.exitCode != 0) { if (result.exitCode != 0) {
throw ProcessException( throw ProcessException(
command, command,

View File

@ -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<String> arguments;
final bool usesBashShim;
}
_ResolvedCommand _resolveCommand(String executable, List<String> 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<ProcessResult> runExternalCommand(
String executable,
List<String> arguments, {
String? workingDirectory,
Map<String, String>? 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<Process> startExternalCommand(
String executable,
List<String> arguments, {
String? workingDirectory,
Map<String, String>? 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<String, String>? _effectiveEnvironmentForResolvedCommand({
required Map<String, String>? environment,
required bool includeParentEnvironment,
required bool usesBashShim,
}) {
if (!usesBashShim || environment == null || environment.isEmpty) {
return environment;
}
final effectiveEnvironment = Map<String, String>.from(environment);
final existingWslenvFromProcess = includeParentEnvironment
? Platform.environment['WSLENV']
: null;
final existingWslenv =
effectiveEnvironment['WSLENV'] ?? existingWslenvFromProcess;
final forwardedKeys = <String>{
...?existingWslenv?.split(':').where((entry) => entry.isNotEmpty),
...effectiveEnvironment.keys.where((key) => key != 'WSLENV'),
};
effectiveEnvironment['WSLENV'] = forwardedKeys.join(':');
return effectiveEnvironment;
}

View File

@ -4,7 +4,7 @@ version: 1.0.0
# repository: https://github.com/my_org/my_repo # repository: https://github.com/my_org/my_repo
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.3
dependencies: dependencies:
args: ^2.7.0 args: ^2.7.0
@ -30,3 +30,11 @@ dev_dependencies:
json_serializable: ">=6.11.1 <6.12.0" json_serializable: ">=6.11.1 <6.12.0"
lints: ^6.0.0 lints: ^6.0.0
test: ^1.25.6 test: ^1.25.6
platforms:
# android:
# ios:
linux:
macos:
# web:
windows:

View File

@ -194,77 +194,77 @@
"dart-api-design": { "dart-api-design": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "d60beeedc91e1be7c51d7fea38acb636c0519b8140159402afcf69f31846cfba" "computedHash": "00f2a541ca05387967b6e44afbce8b3e235a45225e06a376ec98e91116b76c09"
}, },
"dart-async-programming": { "dart-async-programming": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "ebff987d2ce82a0c0eebab1b7ebc724d29bb925c56273ee55775550d634a3c72" "computedHash": "52f989f2cb1121b8c5122d4cf6c9dbe7283523e7144e67e1f86640d33a82ce28"
}, },
"dart-code-generation": { "dart-code-generation": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "259e371481a822984e666cc5214c97ac1581a0adbbdb4d7cab3997c9b1de2009" "computedHash": "815a84815bbe6681129a6d883278fa6d251d3f4e5a6509e8e1ca74cab4422279"
}, },
"dart-compilation-deployment": { "dart-compilation-deployment": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "862ea7fd5efb9bd9760d9eab58880a1f08bb9b64125f171b778ba6918f79980f" "computedHash": "b327d665fd5514cd0e1a193c8c51745405d7476a5b8419c5d9228324b6afa7d9"
}, },
"dart-concurrency-isolates": { "dart-concurrency-isolates": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "829a819bb37342813eca318e0008d93db83aa313daa58be1b9a508d5b1674a9f" "computedHash": "a3e7ff69a58c462d3ea94d7092d1d03e21eeb43bdbb21174400a111da4a96721"
}, },
"dart-documentation": { "dart-documentation": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "0016010518730eb2aeaabbf5046667deb724040be855a5892db4a9735608ca75" "computedHash": "5eb2845edf402683555208ede425b217484fc1fe21f63131fefe9b249f97697a"
}, },
"dart-effective-style": { "dart-effective-style": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "ceaa8cbe82090bf759d763cdc83aa80df79ac2461d0d5f223f63e72f60901ae4" "computedHash": "3b11a0ad963151c12bbf0302cd526dcfdfa6659e194455f168d8441fae60e895"
}, },
"dart-idiomatic-usage": { "dart-idiomatic-usage": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "ac383cc321ce8eeac6412eecc466e68adfcb131fd98c5b74af9d4b33c64381bf" "computedHash": "c45023b434e63316dd209afc094f7412fd99683869b917f140918d0c065406c8"
}, },
"dart-language-syntax": { "dart-language-syntax": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "d1b1f4027530bb736247f62135845c06f69148cd3464fc235f942324499aee54" "computedHash": "457294ca1c8e21c0ceb643ded8dfa1a2df1a542bea5825c95d331b99d119bd25"
}, },
"dart-migration-versioning": { "dart-migration-versioning": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "c3fefb316b55d7794a49bc226cee17a2fafc00d75621e23aee1dd3f8c15def3a" "computedHash": "f94fb07b64c2a4f1922504bf4de531de927e076a74b3cbd291a75763c3a7d6cb"
}, },
"dart-native-interop-ffi": { "dart-native-interop-ffi": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "cac814c48f43b964471b41e99622b471203bb2900845bcc03eca02b2a3162ed5" "computedHash": "0e29e6b5e8cb803d7644c6f13340cc1e3265d23f068be4da0f0781954737b3c6"
}, },
"dart-package-management": { "dart-package-management": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "a1fbb5422c3d9bf980a310b55f8cb938b969234eda60a27038f65833e5d0e805" "computedHash": "569d2a0bcb4b61a15fb8e0f3406d9c54ddc9e1caf44c92302047b91730bd9320"
}, },
"dart-static-analysis": { "dart-static-analysis": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "f13d7c751ed2d026da741fe7b2535fb11a8d045c48ee91b48c0bf7b8147a3758" "computedHash": "6258439460032ed8204d18abb0f09112ad7969d2497221693b302ce800f12106"
}, },
"dart-testing": { "dart-testing": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "c6857b34f27d704eadbb770e40512693e18e04bab7345c23818386a6996dea6a" "computedHash": "2d8a5d92cfa90dd5ad13f8f38606bdcb6edec8fc344fde02f51475a29f05d87a"
}, },
"dart-web-development": { "dart-web-development": {
"source": "dart-lang/skills", "source": "dart-lang/skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "2adc4b77a7d30a2c9c52fc96368df10a8242512c95c8bf2b3fc06abf208dc421" "computedHash": "db064015bed95259c6f60ba6eb5123a97d6341e68a0925a60a3d57a31bf71d6c"
}, },
"datanalysis-credit-risk": { "datanalysis-credit-risk": {
"source": "github/awesome-copilot", "source": "github/awesome-copilot",
@ -839,7 +839,7 @@
"supabase-postgres-best-practices": { "supabase-postgres-best-practices": {
"source": "supabase/agent-skills", "source": "supabase/agent-skills",
"sourceType": "github", "sourceType": "github",
"computedHash": "9c87c315aed143ee3b34bec8117100f5035e0df09e6b23e1ecc772cff434c9ad" "computedHash": "f4cd7570115d97af6bcb2946eb891a958c8d04a43b479871c9ccadbc670ad058"
}, },
"technology-stack-blueprint-generator": { "technology-stack-blueprint-generator": {
"source": "github/awesome-copilot", "source": "github/awesome-copilot",
@ -944,7 +944,7 @@
"worktrunk": { "worktrunk": {
"source": "max-sixty/worktrunk", "source": "max-sixty/worktrunk",
"sourceType": "github", "sourceType": "github",
"computedHash": "12ed42480bc641bb017955e0884716ccf9d04389e744edad6dfcac4b01c60cca" "computedHash": "d480468e16c7de39a121e6e760788357bb178495f8912b05f05f55977468d72e"
}, },
"write-coding-standards-from-file": { "write-coding-standards-from-file": {
"source": "github/awesome-copilot", "source": "github/awesome-copilot",

View File

@ -5,6 +5,40 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; 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() { void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
@ -358,16 +392,11 @@ projects:
// When loading the app config from disk. // When loading the app config from disk.
final config = await AppConfig.load(configFile.path); final config = await AppConfig.load(configFile.path);
final home = _resolveTestHomeDirectory();
// Then the app exposes the configured data and worktree directories. // Then the app exposes the configured data and worktree directories.
expect( expect(config.dataDir, p.join(home, '.agent-orchestrator'));
config.dataDir, expect(config.worktreeDir, p.join(home, '.worktrees'));
p.join(Platform.environment['HOME']!, '.agent-orchestrator'),
);
expect(
config.worktreeDir,
p.join(Platform.environment['HOME']!, '.worktrees'),
);
// And each project keeps the configured camelCase settings. // And each project keeps the configured camelCase settings.
expect(config.projects['sample'], isNotNull); expect(config.projects['sample'], isNotNull);
@ -809,24 +838,19 @@ projects:
// When writing the generated starter config to disk. // When writing the generated starter config to disk.
await configFile.writeAsString(contents); await configFile.writeAsString(contents);
final config = await AppConfig.load(configFile.path); final config = await AppConfig.load(configFile.path);
final home = _resolveTestHomeDirectory();
// Then the file parses through the normal app config loader. // Then the file parses through the normal app config loader.
expect(config.projects['sample'], isNotNull); expect(config.projects['sample'], isNotNull);
// And the required starter values stay available to users. // And the required starter values stay available to users.
expect( expect(config.dataDir, p.join(home, '.agent-orchestrator'));
config.dataDir,
p.join(Platform.environment['HOME']!, '.agent-orchestrator'),
);
expect(config.projects['sample']!.provider, IssueTrackerProvider.github); expect(config.projects['sample']!.provider, IssueTrackerProvider.github);
expect( expect(config.worktreeDir, p.join(home, '.worktrees'));
config.worktreeDir,
p.join(Platform.environment['HOME']!, '.worktrees'),
);
expect(config.projects['sample']!.repo, 'owner/repo'); expect(config.projects['sample']!.repo, 'owner/repo');
expect( expect(
config.projects['sample']!.path, 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']!.defaultBranch, 'main');
expect(config.projects['sample']!.issueAssistant.mentionTriggers, [ expect(config.projects['sample']!.issueAssistant.mentionTriggers, [

View File

@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunOnceConcurrencyTests() { void registerIssueAssistantAppRunOnceConcurrencyTests() {
/// ```gherkin /// ```gherkin

View File

@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunOnceCoreTests() { void registerIssueAssistantAppRunOnceCoreTests() {
/// ```gherkin /// ```gherkin

View File

@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunOnceMultiProjectTests() { void registerIssueAssistantAppRunOnceMultiProjectTests() {
/// ```gherkin /// ```gherkin

View File

@ -6,7 +6,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunRetryTests() { void registerIssueAssistantAppRunRetryTests() {
/// ```gherkin /// ```gherkin

View File

@ -76,7 +76,7 @@ void registerIssueAssistantAppRunGosmeeTests() {
final postedBody = File(p.join(sandbox.path, 'posted-body.md')); final postedBody = File(p.join(sandbox.path, 'posted-body.md'));
await teaScript.writeAsString('''#!/usr/bin/env bash await teaScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
printf '%s\n' "\$*" >> "${teaLog.path}" printf '%s\n' "\$*" >> "${bashScriptPath(teaLog.path)}"
if [[ "\$1" == "api" && "\$2" == "user" ]]; then if [[ "\$1" == "api" && "\$2" == "user" ]]; then
cat <<'JSON' cat <<'JSON'
@ -102,7 +102,7 @@ fi
if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/12/comments" ]]; then if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/12/comments" ]]; then
for arg in "\$@"; do for arg in "\$@"; do
if [[ "\$arg" == body=* ]]; then if [[ "\$arg" == body=* ]]; then
printf '%s' "\${arg#body=}" > "${postedBody.path}" printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}"
fi fi
done done
cat <<'JSON' cat <<'JSON'
@ -125,7 +125,7 @@ fi
echo "unexpected tea args: \$*" >&2 echo "unexpected tea args: \$*" >&2
exit 1 exit 1
'''); ''');
await Process.run('chmod', ['+x', teaScript.path]); await makeScriptExecutable(teaScript);
// And a responder that emits a planning reply. // And a responder that emits a planning reply.
final responderScript = File(p.join(sandbox.path, 'responder.sh')); final responderScript = File(p.join(sandbox.path, 'responder.sh'));
@ -226,7 +226,7 @@ projects:
final gosmeeLog = File(p.join(sandbox.path, 'gosmee-log.jsonl')); final gosmeeLog = File(p.join(sandbox.path, 'gosmee-log.jsonl'));
await gosmeeScript.writeAsString('''#!/usr/bin/env bash await gosmeeScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail 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 if [[ "\$1" == "--output" && "\$2" == "json" && "\$3" == "client" && "\$4" == "--new-url" ]]; then
printf '%s\n' 'https://gosmee.example.test/reconcile' printf '%s\n' 'https://gosmee.example.test/reconcile'
@ -242,7 +242,7 @@ fi
echo "unexpected gosmee args: \$*" >&2 echo "unexpected gosmee args: \$*" >&2
exit 1 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. // And a tea script that returns a new issue only on a later reconciliation poll.
final teaScript = File(p.join(sandbox.path, 'tea')); final teaScript = File(p.join(sandbox.path, 'tea'));
@ -272,7 +272,7 @@ exit 1
]; ];
await teaScript.writeAsString('''#!/usr/bin/env bash await teaScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
printf '%s\n' "\$*" >> "${teaLog.path}" printf '%s\n' "\$*" >> "${bashScriptPath(teaLog.path)}"
if [[ "\$1" == "api" && "\$2" == "user" ]]; then if [[ "\$1" == "api" && "\$2" == "user" ]]; then
cat <<'JSON' cat <<'JSON'
@ -283,11 +283,11 @@ fi
if [[ "\$1" == "api" && "\$4" == repos/owner/sample/issues\\?state=open* ]]; then if [[ "\$1" == "api" && "\$4" == repos/owner/sample/issues\\?state=open* ]]; then
count=0 count=0
if [[ -f "${issueListCount.path}" ]]; then if [[ -f "${bashScriptPath(issueListCount.path)}" ]]; then
count="\$(cat "${issueListCount.path}")" count="\$(cat "${bashScriptPath(issueListCount.path)}")"
fi fi
count=\$((count + 1)) count=\$((count + 1))
printf '%s' "\$count" > "${issueListCount.path}" printf '%s' "\$count" > "${bashScriptPath(issueListCount.path)}"
if [[ "\$count" -eq 1 ]]; then if [[ "\$count" -eq 1 ]]; then
cat <<'JSON' cat <<'JSON'
[] []
@ -310,7 +310,7 @@ fi
if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then
for arg in "\$@"; do for arg in "\$@"; do
if [[ "\$arg" == body=* ]]; then if [[ "\$arg" == body=* ]]; then
printf '%s' "\${arg#body=}" > "${postedBody.path}" printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}"
fi fi
done done
cat <<'JSON' cat <<'JSON'
@ -333,7 +333,7 @@ fi
echo "unexpected tea args: \$*" >&2 echo "unexpected tea args: \$*" >&2
exit 1 exit 1
'''); ''');
await Process.run('chmod', ['+x', teaScript.path]); await makeScriptExecutable(teaScript);
// And a responder that emits a planning reply. // And a responder that emits a planning reply.
final responderScript = File(p.join(sandbox.path, 'responder.sh')); final responderScript = File(p.join(sandbox.path, 'responder.sh'));

View File

@ -178,7 +178,7 @@ fi
echo "unexpected gh args: \$*" >&2 echo "unexpected gh args: \$*" >&2
exit 1 exit 1
'''); ''');
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
// And an orchestrator-style config that uses gh/webhook-forward. // And an orchestrator-style config that uses gh/webhook-forward.
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml')); final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
@ -284,7 +284,7 @@ projects:
]; ];
await ghScript.writeAsString('''#!/usr/bin/env bash await ghScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
printf '%s\n' "\$*" >> "${ghLog.path}" printf '%s\n' "\$*" >> "${bashScriptPath(ghLog.path)}"
if [[ "\$1" == "api" && "\$2" == "user" ]]; then if [[ "\$1" == "api" && "\$2" == "user" ]]; then
cat <<'JSON' cat <<'JSON'
@ -295,11 +295,11 @@ fi
if [[ "\$1" == "api" && "\$4" == "search/issues" ]]; then if [[ "\$1" == "api" && "\$4" == "search/issues" ]]; then
count=0 count=0
if [[ -f "${searchCountFile.path}" ]]; then if [[ -f "${bashScriptPath(searchCountFile.path)}" ]]; then
count="\$(cat "${searchCountFile.path}")" count="\$(cat "${bashScriptPath(searchCountFile.path)}")"
fi fi
count=\$((count + 1)) count=\$((count + 1))
printf '%s' "\$count" > "${searchCountFile.path}" printf '%s' "\$count" > "${bashScriptPath(searchCountFile.path)}"
if [[ "\$count" -eq 1 ]]; then if [[ "\$count" -eq 1 ]]; then
cat <<'JSON' cat <<'JSON'
{"total_count":0,"incomplete_results":false,"items":[]} {"total_count":0,"incomplete_results":false,"items":[]}
@ -326,7 +326,7 @@ fi
if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then if [[ "\$1" == "api" && "\$4" == "repos/owner/sample/issues/18/comments" ]]; then
for arg in "\$@"; do for arg in "\$@"; do
if [[ "\$arg" == body=* ]]; then if [[ "\$arg" == body=* ]]; then
printf '%s' "\${arg#body=}" > "${postedBody.path}" printf '%s' "\${arg#body=}" > "${bashScriptPath(postedBody.path)}"
fi fi
done done
cat <<'JSON' cat <<'JSON'
@ -344,7 +344,7 @@ fi
echo "unexpected gh args: \$*" >&2 echo "unexpected gh args: \$*" >&2
exit 1 exit 1
'''); ''');
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
// And a responder that emits a planning reply. // And a responder that emits a planning reply.
final responderScript = File(p.join(sandbox.path, 'responder.sh')); final responderScript = File(p.join(sandbox.path, 'responder.sh'));

View File

@ -7,7 +7,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunOnceNotificationTests() { void registerIssueAssistantAppRunOnceNotificationTests() {
/// ```gherkin /// ```gherkin

View File

@ -4,6 +4,54 @@ import 'dart:io';
import 'package:git/git.dart'; import 'package:git/git.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
Future<void> 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<void> writeFakeGhScript({ Future<void> writeFakeGhScript({
required File ghScript, required File ghScript,
required List<Map<String, Object?>> issueListResponse, required List<Map<String, Object?>> issueListResponse,
@ -71,7 +119,7 @@ Future<void> writeFakeGhScript({
if (ghLog != null) { if (ghLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${ghLog.path}"', '"${bashScriptPath(ghLog.path)}"',
); );
} }
@ -154,7 +202,7 @@ Future<void> writeFakeGhScript({
..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln(r''' if [[ "$arg" == body=* ]]; then''')
..writeln( ..writeln(
r''' printf '%s' "${arg#body=}" > ''' r''' printf '%s' "${arg#body=}" > '''
'"${postedBodyFile.path}"', '"${bashScriptPath(postedBodyFile.path)}"',
) )
..writeln(r''' fi'''); ..writeln(r''' fi''');
} }
@ -179,7 +227,7 @@ Future<void> writeFakeGhScript({
..writeln('exit 1'); ..writeln('exit 1');
await ghScript.writeAsString(buffer.toString()); await ghScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
} }
Future<void> writeFlakySearchGhScript({ Future<void> writeFlakySearchGhScript({
@ -210,7 +258,7 @@ Future<void> writeFlakySearchGhScript({
if (ghLog != null) { if (ghLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${ghLog.path}"', '"${bashScriptPath(ghLog.path)}"',
); );
} }
@ -224,8 +272,10 @@ Future<void> writeFlakySearchGhScript({
buffer buffer
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''') ..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
..writeln(' if [[ ! -f "${failureStateFile.path}" ]]; then') ..writeln(
..writeln(' touch "${failureStateFile.path}"') ' if [[ ! -f "${bashScriptPath(failureStateFile.path)}" ]]; then',
)
..writeln(' touch "${bashScriptPath(failureStateFile.path)}"')
..writeln(r''' echo "error connecting to api.github.com" >&2''') ..writeln(r''' echo "error connecting to api.github.com" >&2''')
..writeln( ..writeln(
r''' echo "check your internet connection or https://githubstatus.com" >&2''', r''' echo "check your internet connection or https://githubstatus.com" >&2''',
@ -256,7 +306,7 @@ Future<void> writeFlakySearchGhScript({
..writeln('exit 1'); ..writeln('exit 1');
await ghScript.writeAsString(buffer.toString()); await ghScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
} }
Future<void> writeFakeTeaScript({ Future<void> writeFakeTeaScript({
@ -287,7 +337,7 @@ Future<void> writeFakeTeaScript({
if (teaLog != null) { if (teaLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${teaLog.path}"', '"${bashScriptPath(teaLog.path)}"',
); );
} }
@ -352,7 +402,7 @@ Future<void> writeFakeTeaScript({
..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln(r''' if [[ "$arg" == body=* ]]; then''')
..writeln( ..writeln(
r''' printf '%s' "${arg#body=}" > ''' r''' printf '%s' "${arg#body=}" > '''
'"${postedBodyFile.path}"', '"${bashScriptPath(postedBodyFile.path)}"',
) )
..writeln(r''' fi'''); ..writeln(r''' fi''');
} }
@ -377,7 +427,7 @@ Future<void> writeFakeTeaScript({
..writeln('exit 1'); ..writeln('exit 1');
await teaScript.writeAsString(buffer.toString()); await teaScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', teaScript.path]); await makeScriptExecutable(teaScript);
} }
Future<void> writeResponderScript( Future<void> writeResponderScript(
@ -395,7 +445,7 @@ ${jsonEncode(response)}
JSON JSON
exit $exitCode exit $exitCode
'''); ''');
await Process.run('chmod', ['+x', responderScript.path]); await makeScriptExecutable(responderScript);
} }
Future<void> writeJsonlResponderScript( Future<void> writeJsonlResponderScript(
@ -412,7 +462,7 @@ cat <<'JSON'
{"type":"turn.completed"} {"type":"turn.completed"}
JSON JSON
'''); ''');
await Process.run('chmod', ['+x', responderScript.path]); await makeScriptExecutable(responderScript);
} }
Future<void> writeDelayedLoggingResponderScript( Future<void> writeDelayedLoggingResponderScript(
@ -429,15 +479,15 @@ import time
print(int(time.time() * 1000)) print(int(time.time() * 1000))
PY 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 cat >/dev/null
sleep ${delay.inSeconds} 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' cat <<'JSON'
${jsonEncode(response)} ${jsonEncode(response)}
JSON JSON
'''); ''');
await Process.run('chmod', ['+x', responderScript.path]); await makeScriptExecutable(responderScript);
} }
Future<void> writeWebhookForwardGhScript({ Future<void> writeWebhookForwardGhScript({
@ -478,7 +528,7 @@ Future<void> writeWebhookForwardGhScript({
if (ghLog != null) { if (ghLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${ghLog.path}"', '"${bashScriptPath(ghLog.path)}"',
); );
} }
@ -521,7 +571,7 @@ Future<void> writeWebhookForwardGhScript({
..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln(r''' if [[ "$arg" == body=* ]]; then''')
..writeln( ..writeln(
r''' printf '%s' "${arg#body=}" > ''' r''' printf '%s' "${arg#body=}" > '''
'"${postedBodyFile.path}"', '"${bashScriptPath(postedBodyFile.path)}"',
) )
..writeln(r''' fi'''); ..writeln(r''' fi''');
} }
@ -567,7 +617,7 @@ Future<void> writeWebhookForwardGhScript({
..writeln('exit 1'); ..writeln('exit 1');
await ghScript.writeAsString(buffer.toString()); await ghScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
} }
Future<void> writeGitHubGosmeeGhScript({ Future<void> writeGitHubGosmeeGhScript({
@ -601,7 +651,7 @@ Future<void> writeGitHubGosmeeGhScript({
if (ghLog != null) { if (ghLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${ghLog.path}"', '"${bashScriptPath(ghLog.path)}"',
); );
} }
@ -668,7 +718,7 @@ Future<void> writeGitHubGosmeeGhScript({
..writeln(r''' if [[ "$arg" == body=* ]]; then''') ..writeln(r''' if [[ "$arg" == body=* ]]; then''')
..writeln( ..writeln(
r''' printf '%s' "${arg#body=}" > ''' r''' printf '%s' "${arg#body=}" > '''
'"${postedBodyFile.path}"', '"${bashScriptPath(postedBodyFile.path)}"',
) )
..writeln(r''' fi'''); ..writeln(r''' fi''');
} }
@ -698,7 +748,7 @@ Future<void> writeGitHubGosmeeGhScript({
..writeln('exit 1'); ..writeln('exit 1');
await ghScript.writeAsString(buffer.toString()); await ghScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', ghScript.path]); await makeScriptExecutable(ghScript);
} }
Future<void> writeGosmeeForwardScript({ Future<void> writeGosmeeForwardScript({
@ -714,7 +764,7 @@ Future<void> writeGosmeeForwardScript({
if (gosmeeLog != null) { if (gosmeeLog != null) {
buffer.writeln( buffer.writeln(
r'''printf '%s\n' "$*" >> ''' r'''printf '%s\n' "$*" >> '''
'"${gosmeeLog.path}"', '"${bashScriptPath(gosmeeLog.path)}"',
); );
} }
@ -757,7 +807,7 @@ Future<void> writeGosmeeForwardScript({
..writeln('exit 1'); ..writeln('exit 1');
await gosmeeScript.writeAsString(buffer.toString()); await gosmeeScript.writeAsString(buffer.toString());
await Process.run('chmod', ['+x', gosmeeScript.path]); await makeScriptExecutable(gosmeeScript);
} }
Future<void> initGitRepo(Directory directory) async { Future<void> initGitRepo(Directory directory) async {

View File

@ -4,7 +4,7 @@ import 'package:code_work_spawner/code_work_spawner.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void main() { void main() {
/// ```gherkin /// ```gherkin

View File

@ -5,7 +5,7 @@ import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'test_support.dart'; import '../test_support.dart';
void registerIssueAssistantAppRunOnceWorktreeTests() { void registerIssueAssistantAppRunOnceWorktreeTests() {
/// ```gherkin /// ```gherkin
@ -78,13 +78,13 @@ void registerIssueAssistantAppRunOnceWorktreeTests() {
); );
await responderScript.writeAsString('''#!/usr/bin/env bash await responderScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
pwd > "${cwdCapture.path}" pwd > "${bashScriptPath(cwdCapture.path)}"
cat > "${stdinCapture.path}" cat > "${bashScriptPath(stdinCapture.path)}"
cat <<'JSON' cat <<'JSON'
{"decision":"no_reply","mode":"planning","summary":"captured mention context"} {"decision":"no_reply","mode":"planning","summary":"captured mention context"}
JSON JSON
'''); ''');
await Process.run('chmod', ['+x', responderScript.path]); await makeScriptExecutable(responderScript);
// And an orchestrator-style config pointing to the fake repo and responder. // And an orchestrator-style config pointing to the fake repo and responder.
final configFile = File( final configFile = File(
@ -115,7 +115,7 @@ projects:
await app.close(); await app.close();
// Then the responder runs from an ephemeral worktree instead of the source checkout. // 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( expect(
p.normalize(capturedCwd.trim()), p.normalize(capturedCwd.trim()),
isNot(p.normalize(repoDir.path)), isNot(p.normalize(repoDir.path)),
@ -201,13 +201,13 @@ projects:
); );
await responderScript.writeAsString('''#!/usr/bin/env bash await responderScript.writeAsString('''#!/usr/bin/env bash
set -euo pipefail set -euo pipefail
pwd > "${cwdCapture.path}" pwd > "${bashScriptPath(cwdCapture.path)}"
cat >/dev/null cat >/dev/null
cat <<'JSON' cat <<'JSON'
{"decision":"reply","mode":"planning","markdown":"planning reply","summary":"used project path"} {"decision":"reply","mode":"planning","markdown":"planning reply","summary":"used project path"}
JSON JSON
'''); ''');
await Process.run('chmod', ['+x', responderScript.path]); await makeScriptExecutable(responderScript);
// And an orchestrator-style config pointing to the fake repo and responder. // And an orchestrator-style config pointing to the fake repo and responder.
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml')); final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
@ -237,7 +237,7 @@ projects:
await app.close(); await app.close();
// Then the responder runs from an ephemeral worktree instead of the source checkout. // 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.normalize(capturedCwd.trim()), isNot(p.normalize(repoDir.path)));
expect( expect(
p.isWithin(worktreeRoot.path, p.normalize(capturedCwd.trim())), p.isWithin(worktreeRoot.path, p.normalize(capturedCwd.trim())),