fix(env): harden the PATH preset per review (T-511)

Three holes from the T-511 adversarial review pass:

- An entry containing the PATH separator smuggled extra tokens into
  the joined PATH — a stray trailing ':' yields an EMPTY token, which
  POSIX shells resolve as CWD (the dot-in-PATH hazard). The CLI verb
  and the settings control now reject such entries, and applyPathPreset
  skips malformed stored values that predate the check.
- The gitdir pointer a worktree resolution follows is repo-controlled
  text; the resolved main root is now validated (must hold a real
  .git directory) before its preset key is trusted, so a crafted
  pointer can't alias an arbitrary path's preset.
- A pane spawned with a cwd below the workspace root hashed the
  subdirectory and silently missed the workspace preset; the lookup
  now keys any in-workspace cwd to the workspace root
  (presetLookupRoot).

Also: the Add button pairs buttonBackground with its own
buttonHoverBackground token instead of borrowing the list-item hover
token, and the hosted-Claude leg gains an end-to-end orchestrator test
(preset lookup → spawn env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 18:17:35 +02:00
co-authored by Claude Fable 5
parent 49c82556a2
commit 0a44f95dfd
8 changed files with 157 additions and 17 deletions
@@ -81,6 +81,29 @@ void main() {
expect(dirs(f.tempDir.path), isEmpty);
});
testWidgets('an entry containing a PATH separator is rejected (CWD-token guard)', (tester) async {
await tester.runAsync(() => f.services.settings.setProjectDir(f.tempDir));
await pump(tester);
await tester.pump();
await tester.enterText(find.byType(EditableText), '/opt/go/bin:');
await tester.tap(find.text('Add entry'));
await tester.pump();
expect(find.textContaining('absolute path'), findsOneWidget);
expect(dirs(f.tempDir.path), isEmpty);
});
testWidgets('~/ expands against HOME and Enter submits (parity with the CLI verb)', (tester) async {
final home = Platform.environment['HOME'];
if (home == null || home.isEmpty) return; // no HOME in this environment — the guard path is CLI-tested
await tester.runAsync(() => f.services.settings.setProjectDir(f.tempDir));
await pump(tester);
await tester.pump();
await tester.enterText(find.byType(EditableText), '~/go/bin/');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(dirs(f.tempDir.path), ['$home/go/bin']);
});
testWidgets('an existing dir renders without the missing tag', (tester) async {
await tester.runAsync(() async {
await f.services.settings.setProjectDir(f.tempDir);