@@ -55,17 +55,20 @@ final class _FakeGithubApi extends Fake implements GithubApi {
5555}
5656
5757final class _SessionRecordingConnection extends RecordingConnection
58- implements ConnectionSessionStampSource {
58+ implements ConnectionSessionStampSource, ConnectionDirectoryListingSource {
5959 _SessionRecordingConnection({
6060 List<List<RemoteEntry>>? directoryListings,
61+ List<bool>? listingTruncation,
6162 this.failPutAt,
6263 this.holdPutAt,
6364 }) : directoryListings =
6465 directoryListings ?? <List<RemoteEntry>>[<RemoteEntry>[]],
66+ listingTruncation = listingTruncation ?? <bool>[false],
6567 _sessionStamp = Object(),
6668 super(initial: ConnState.ready);
6769
6870 final List<List<RemoteEntry>> directoryListings;
71+ final List<bool> listingTruncation;
6972 final int? failPutAt;
7073 final int? holdPutAt;
7174
@@ -82,14 +85,28 @@ final class _SessionRecordingConnection extends RecordingConnection
8285 void advanceSession() => _sessionStamp = Object();
8386
8487 @override
85- Future<List<RemoteEntry>> listDir (String path) async {
88+ Future<DirectoryListing> listDirWithMetadata (String path) async {
8689 operationLog.add('list:$path');
87- if (directoryListings.isEmpty) return <RemoteEntry>[];
90+ if (directoryListings.isEmpty) {
91+ return const DirectoryListing(entries: <RemoteEntry>[], truncated: false);
92+ }
8893 final int index = _listIndex < directoryListings.length
8994 ? _listIndex
9095 : directoryListings.length - 1;
9196 _listIndex += 1;
92- return List<RemoteEntry>.of(directoryListings[index]);
97+ final int truncationIndex = index < listingTruncation.length
98+ ? index
99+ : listingTruncation.length - 1;
100+ return DirectoryListing(
101+ entries: directoryListings[index],
102+ truncated: listingTruncation[truncationIndex],
103+ );
104+ }
105+
106+ @override
107+ Future<List<RemoteEntry>> listDir(String path) async {
108+ final DirectoryListing listing = await listDirWithMetadata(path);
109+ return listing.entries;
93110 }
94111
95112 @override
@@ -133,13 +150,15 @@ final class _Harness {
133150 factory _Harness({
134151 String cwd = '/flash',
135152 List<List<RemoteEntry>>? directoryListings,
153+ List<bool>? listingTruncation,
136154 int? failPutAt,
137155 int? holdPutAt,
138156 }) {
139157 // Share one chronological log so tests can prove that no PUT races ahead
140158 // of a pending or unvalidated Git blob.
141159 final _SessionRecordingConnection connection = _SessionRecordingConnection(
142160 directoryListings: directoryListings,
161+ listingTruncation: listingTruncation,
143162 failPutAt: failPutAt,
144163 holdPutAt: holdPutAt,
145164 );
@@ -217,12 +236,14 @@ void main() {
217236 _Harness harness({
218237 String cwd = '/flash',
219238 List<List<RemoteEntry>>? directoryListings,
239+ List<bool>? listingTruncation,
220240 int? failPutAt,
221241 int? holdPutAt,
222242 }) {
223243 final _Harness value = _Harness(
224244 cwd: cwd,
225245 directoryListings: directoryListings,
246+ listingTruncation: listingTruncation,
226247 failPutAt: failPutAt,
227248 holdPutAt: holdPutAt,
228249 );
@@ -368,6 +389,25 @@ void main() {
368389 expect(h.connection.putFileCalls, isEmpty);
369390 });
370391
392+ test(
393+ 'a truncated board listing blocks review before any download',
394+ () async {
395+ final _Harness h = harness(
396+ directoryListings: <List<RemoteEntry>>[
397+ <RemoteEntry>[_remoteFile('visible.py')],
398+ ],
399+ listingTruncation: <bool>[true],
400+ );
401+
402+ await expectLater(
403+ h.importer.review(_repository, <GithubEntry>[_file('hidden.py')]),
404+ throwsA(_failure(GithubFailureKind.incompleteBoardListing)),
405+ );
406+ expect(h.api.fetchedRemotePaths, isEmpty);
407+ expect(h.connection.putFileCalls, isEmpty);
408+ },
409+ );
410+
371411 test(
372412 'an existing file requires separate explicit overwrite consent',
373413 () async {
0 commit comments