Skip to content

Commit 069666e

Browse files
committed
Merge main into feat/base-branch (Forgejo + SPEC-51 resolution)
2 parents fbcad33 + a3d3da5 commit 069666e

252 files changed

Lines changed: 36064 additions & 527 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ app/test/sim/frames/
2424

2525
# Local QA artifacts (screenshots, issue logs) from tool/shoot-ports.sh
2626
.qa/
27+
.piano/
28+
29+
# Local pnpm store, created when a store-dir is set for this checkout
30+
.pnpm-store/

.pnpm-store/v11/index.db

8 KB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../server

app/ASSET_ATTRIBUTION.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,26 @@ uses a neutral Phosphor glyph for those rather than a fabricated mark.
2020
| File(s) | Role | Source | License |
2121
|---------|------|--------|---------|
2222
| `repo-push.png`, `repo-pull.png` | Composer PR actions | [VS Code codicons](https://github.com/microsoft/vscode-codicons) | CC BY 4.0 |
23-
| `git-pull-request-closed-{thin,light,regular,bold,fill}.svg` | Closed-PR state marker | Original, drawn to match [Phosphor](https://phosphoricons.com) metrics (five weights: light is default) | MIT (this repo) |
23+
| `git-pull-request-closed-{thin,light,regular,bold,fill}.svg` | Closed-PR state marker | Original — [`phosphor_extras`](../../phosphor_extras) | MIT |
24+
| `forgejo-light.svg` | Forgejo forge marker | Original reduction of Forgejo's mark to [Phosphor](https://phosphoricons.com) metrics — [`phosphor_extras`](../../phosphor_extras) | MIT (drawing); Forgejo's mark belongs to the Forgejo project |
25+
| `gitea-light.svg` | Gitea forge marker | Original reduction of Gitea's mark to Phosphor metrics — [`phosphor_extras`](../../phosphor_extras) | MIT (drawing); Gitea's mark belongs to the Gitea project |
26+
27+
Glyphs marked *Original* are authored in the **`phosphor_extras`** repo, which is
28+
the source of truth for their geometry and holds the generator and the invariant
29+
checks. `scripts/sync-icons.sh` vendors the built SVGs here (rather than adding a
30+
dependency) so a fresh clone builds without network access to another repo;
31+
`scripts/sync-icons.sh --check` fails if a vendored copy has drifted.
32+
33+
The Forgejo and Gitea glyphs identify those projects' software in the UI —
34+
nominative use. The MIT grant covers our drawings, not the underlying marks.
35+
36+
## Agent logos in [`assets/agents/`](assets/agents/)
37+
38+
Used to identify which coding agent backs a session. Each is the property of its
39+
project and is included for identification only.
40+
41+
| File | Agent | Owner |
42+
|------|-------|-------|
43+
| `claude.svg` | Claude Code | Anthropic |
44+
| `codex.svg` | Codex | OpenAI |
45+
| `pi.svg` | pi | Earendil Works |

app/assets/icons/forgejo-light.svg

Lines changed: 14 additions & 0 deletions
Loading

app/assets/icons/gitea-light.svg

Lines changed: 14 additions & 0 deletions
Loading
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// SPEC-48 T6.2 — the per-repo Settings path, mounted for real.
2+
//
3+
// What this proves that the unit and widget tests cannot: the WHOLE path from the
4+
// live repo snapshot through the dynamic registry to a rendered row, inside the
5+
// real `SettingsWindow`, on a real macOS build.
6+
//
7+
// reposProvider → sectionsFor() → the nav pane → tap → RepositorySettingsPage
8+
// → repoSettingsViewFor() → RepositorySettingsSection → the rows
9+
//
10+
// Every hop above is covered by a unit or widget test in isolation. None of them
11+
// covers the composition, and this repo has already been bitten there once: the
12+
// desktop shell mounts Settings *outside* a GoRouter, so a section that navigated
13+
// with `context.go` threw at runtime while every test stayed green. A test that
14+
// mounts the section directly cannot catch that class of fault; this one can.
15+
//
16+
// Deliberately NOT extended onto the daemon control socket (see the plan's T6.2):
17+
// the daemon-side behaviour is proven by the server tests, and routing this through
18+
// the socket would add infrastructure for no extra coverage. The repo snapshot is
19+
// stubbed at `reposProvider`, which is exactly the seam `SettingsWindow` reads.
20+
//
21+
// Run: app/tool/e2e-desktop-settings.sh
22+
//
23+
// ignore_for_file: depend_on_referenced_packages
24+
import 'dart:io' show Platform;
25+
26+
import 'package:flutter/material.dart';
27+
import 'package:flutter_riverpod/flutter_riverpod.dart';
28+
import 'package:flutter_test/flutter_test.dart';
29+
import 'package:integration_test/integration_test.dart';
30+
import 'package:makit/desktop/daemon/daemon_lifecycle.dart';
31+
import 'package:makit/desktop/desktop_app.dart' show desktopControllerProvider;
32+
import 'package:makit/desktop/desktop_controller.dart';
33+
import 'package:makit/desktop/screens/fake_control_client.dart';
34+
import 'package:makit/desktop/settings/sections/repository_section.dart';
35+
import 'package:makit/desktop/settings/server_config.dart';
36+
import 'package:makit/desktop/settings/settings_nav_pane.dart';
37+
import 'package:makit/desktop/settings/settings_window.dart';
38+
import 'package:makit/store/connection.dart';
39+
import 'package:makit/store/models.dart';
40+
import 'package:makit/store/store.dart';
41+
import 'package:makit/ui/home/repo_monogram.dart';
42+
import 'package:shared_preferences/shared_preferences.dart';
43+
44+
/// Two pinned repos and one unpinned, so the pinned filter is exercised by the
45+
/// same fixture that exercises the rows.
46+
///
47+
/// `Diana` carries a worktree-root override and a chosen hue; `makit` inherits
48+
/// everything. The difference is the point: the section has to report each repo's
49+
/// own state, and a bug that reads the wrong repo's settings passes any fixture
50+
/// where both repos look the same.
51+
/// The real home, because the section abbreviates paths against `HOME` at runtime.
52+
/// Hardcoding `/Users/le` made the `~/trees/diana` and `~/.worktrees` assertions hold
53+
/// on exactly one machine.
54+
final String _home = Platform.environment['HOME'] ?? '/root';
55+
56+
final _repos = <RepoInfo>[
57+
RepoInfo.fromJson({
58+
'id': 'p-diana',
59+
'name': 'Diana',
60+
'path': '$_home/Work/XDent/Diana',
61+
'pinned': true,
62+
'isGitRepo': true,
63+
'defaultBranch': 'main',
64+
'currentBranch': 'main',
65+
'worktrees': const <Map<String, dynamic>>[],
66+
'settings': {
67+
'worktreeRoot': {'value': '$_home/trees/diana', 'source': 'override'},
68+
'provider': {'value': 'forgejo', 'source': 'override'},
69+
'defaultBranch': {'value': 'trunk', 'source': 'override'},
70+
'logoHue': 2,
71+
'hasRemote': true,
72+
'forge': {
73+
'software': 'forgejo',
74+
'host': 'forgejo.internal.test',
75+
'authed': true,
76+
},
77+
},
78+
})!,
79+
RepoInfo.fromJson({
80+
'id': 'p-makit',
81+
'name': 'makit',
82+
'path': '$_home/Work/makit',
83+
'pinned': true,
84+
'isGitRepo': true,
85+
'defaultBranch': 'main',
86+
'currentBranch': 'main',
87+
'worktrees': const <Map<String, dynamic>>[],
88+
'settings': {
89+
'worktreeRoot': {'value': '$_home/.worktrees', 'source': 'default'},
90+
'provider': {'value': 'auto', 'source': 'default'},
91+
'hasRemote': true,
92+
},
93+
})!,
94+
RepoInfo.fromJson({
95+
'id': 'p-noticed',
96+
'name': 'noticed',
97+
'path': '/tmp/noticed',
98+
'pinned': false,
99+
'isGitRepo': true,
100+
'worktrees': const <Map<String, dynamic>>[],
101+
})!,
102+
];
103+
104+
late SharedPreferences _prefs;
105+
106+
Widget _app() => ProviderScope(
107+
overrides: [
108+
reposProvider.overrideWithValue(ReposState(_repos)),
109+
serverConfigProvider.overrideWith(
110+
(ref) => ServerConfigController(_prefs, const ServerConfig()),
111+
),
112+
desktopControllerProvider.overrideWithValue(
113+
DesktopController(
114+
client: FakeControlClient(),
115+
lifecycle: DaemonLifecycle(
116+
resolver: MakitCliResolver(shellLookup: () async => null),
117+
),
118+
),
119+
),
120+
connectionProvider.overrideWithValue(MakitConnState()),
121+
],
122+
child: MaterialApp(home: SettingsWindow(onClose: () {})),
123+
);
124+
125+
Future<void> _openRepo(WidgetTester tester, String name) async {
126+
final row = find.descendant(
127+
of: find.byType(SettingsNavPane),
128+
matching: find.text(name),
129+
);
130+
await tester.ensureVisible(row);
131+
await tester.pumpAndSettle();
132+
await tester.tap(row);
133+
await tester.pumpAndSettle();
134+
}
135+
136+
void main() {
137+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
138+
139+
setUp(() async {
140+
SharedPreferences.setMockInitialValues({});
141+
_prefs = await SharedPreferences.getInstance();
142+
});
143+
144+
testWidgets('a pinned repo gets a reachable section in the real window', (
145+
tester,
146+
) async {
147+
await tester.pumpWidget(_app());
148+
await tester.pumpAndSettle();
149+
150+
// The sidebar lists the pinned repos and not the merely-noticed one.
151+
expect(find.text('Diana'), findsWidgets);
152+
expect(find.text('makit'), findsWidgets);
153+
expect(find.text('noticed'), findsNothing);
154+
155+
await _openRepo(tester, 'Diana');
156+
157+
// The section rendered — not an empty pane, and not the fallback section.
158+
// Group headers are upper-cased by `SettingsSectionHeader`, so these assert the
159+
// rendered string rather than the source one.
160+
expect(find.byType(RepositorySettingsSection), findsOneWidget);
161+
expect(find.text('IDENTITY'), findsOneWidget);
162+
expect(find.text('WORKTREES'), findsOneWidget);
163+
// And the rows themselves, so "the section mounted" is not mistaken for "the
164+
// section rendered its contents".
165+
expect(find.text('Logo'), findsOneWidget);
166+
expect(find.text('Git provider'), findsOneWidget);
167+
expect(find.text('Worktree root'), findsOneWidget);
168+
});
169+
170+
testWidgets('the rows carry THIS repo\'s values, resolved from the snapshot', (
171+
tester,
172+
) async {
173+
await tester.pumpWidget(_app());
174+
await tester.pumpAndSettle();
175+
await _openRepo(tester, 'Diana');
176+
177+
// The override, home-abbreviated, and the badge that distinguishes it from an
178+
// inherited root — the one row where that distinction is the whole feature.
179+
expect(find.text('~/trees/diana'), findsOneWidget);
180+
expect(find.text('overridden'), findsOneWidget);
181+
// The provider override relabels the row rather than reporting detection.
182+
expect(find.textContaining('Set to Forgejo'), findsOneWidget);
183+
// The default-branch override wins over the DTO's git-derived `main`.
184+
expect(find.text('trunk'), findsOneWidget);
185+
});
186+
187+
testWidgets('switching repos re-renders from the newly selected repo', (
188+
tester,
189+
) async {
190+
// The bug this guards: a section built once and cached would keep showing the
191+
// first repo's values under the second repo's title, which reads as correct.
192+
await tester.pumpWidget(_app());
193+
await tester.pumpAndSettle();
194+
195+
await _openRepo(tester, 'Diana');
196+
expect(find.text('overridden'), findsOneWidget);
197+
198+
await _openRepo(tester, 'makit');
199+
expect(
200+
find.text('~/trees/diana'),
201+
findsNothing,
202+
reason: "Diana's root leaked into makit",
203+
);
204+
expect(
205+
find.text('overridden'),
206+
findsNothing,
207+
reason: 'makit inherits, so nothing is overridden',
208+
);
209+
expect(find.text('~/.worktrees'), findsOneWidget);
210+
});
211+
212+
testWidgets('the sidebar draws each repo its own mark, with the chosen hue', (
213+
tester,
214+
) async {
215+
await tester.pumpWidget(_app());
216+
await tester.pumpAndSettle();
217+
218+
final marks = find.descendant(
219+
of: find.byType(SettingsNavPane),
220+
matching: find.byType(RepoMonogram),
221+
);
222+
expect(marks, findsNWidgets(2), reason: 'one mark per pinned repo');
223+
final diana = tester
224+
.widgetList<RepoMonogram>(marks)
225+
.firstWhere((m) => m.name == 'Diana');
226+
expect(diana.hue, 2, reason: 'the stored hue must reach the sidebar');
227+
});
228+
229+
testWidgets('search reaches a repo row and lands on THAT repo', (
230+
tester,
231+
) async {
232+
// The nav pane searches the DYNAMIC sections; on the static list "worktree root"
233+
// found nothing and the result would have been titled `repo:<id>`.
234+
//
235+
// Landing on the RIGHT repo is asserted, not just landing on a repo section: with
236+
// two pinned repos the query matches both, so tapping `.first` and checking only
237+
// that some repo section rendered would pass even if the search sent the user to
238+
// the other repo. The result's subtitle is the repo name, so the tap targets
239+
// Diana specifically and the assertion is on a value only Diana has.
240+
await tester.pumpWidget(_app());
241+
await tester.pumpAndSettle();
242+
243+
await tester.enterText(find.byType(TextField).first, 'worktree root');
244+
await tester.pumpAndSettle();
245+
246+
final results = find.widgetWithText(ListTile, 'Diana');
247+
expect(results, findsOneWidget, reason: 'one matching row for Diana');
248+
await tester.tap(results);
249+
await tester.pumpAndSettle();
250+
251+
expect(find.byType(RepositorySettingsSection), findsOneWidget);
252+
expect(
253+
find.text('~/trees/diana'),
254+
findsOneWidget,
255+
reason: 'landed on Diana, not makit',
256+
);
257+
});
258+
}

app/lib/app/router.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '../status/activity_screen.dart';
77
import '../pairing/onboarding_controller.dart';
88
import '../pairing/onboarding_screen.dart';
99
import '../pairing/readiness.dart';
10+
import '../ui/docs/docs_screen.dart';
1011
import '../ui/home/closed_screen.dart';
1112
import '../ui/home/home_screen.dart';
1213
import '../ui/ports/ports_screen.dart';
@@ -63,6 +64,7 @@ final routerProvider = Provider<GoRouter>((ref) {
6364
builder: (_, s) =>
6465
PortsScreen(repoId: s.uri.queryParameters['repo']),
6566
),
67+
GoRoute(path: 'docs', builder: (_, _) => const DocsScreen()),
6668
GoRoute(
6769
path: 'diagnostics',
6870
builder: (_, _) => const DiagnosticsScreen(),

app/lib/app/routes.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,9 @@ const kRouteActivity = '$kRouteRepos/activity';
2929
/// optional `?repo=<id>` that pre-selects the *This repo* filter.
3030
const kRoutePorts = '$kRouteRepos/ports';
3131

32+
/// The global Docs screen (SPEC-46 P1) — the repo's mockups and specs, grouped
33+
/// repo → worktree.
34+
const kRouteDocs = '$kRouteRepos/docs';
35+
3236
/// A single session's screen.
3337
String routeForSession(String sessionId) => '$kRouteRepos/session/$sessionId';

0 commit comments

Comments
 (0)