xworkmate-app/test/features/assistant/assistant_artifact_sidebar_test.dart
2026-06-07 07:38:04 +08:00

282 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/assistant_artifacts.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/theme/app_theme.dart';
import 'package:xworkmate/widgets/assistant_artifact_sidebar.dart';
void main() {
testWidgets('refreshes snapshot when artifact sync timestamp changes', (
tester,
) async {
var loadCount = 0;
Future<AssistantArtifactSnapshot> loadSnapshot() async {
loadCount += 1;
return AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
fileEntries: <AssistantArtifactEntry>[
AssistantArtifactEntry(
id: 'entry-$loadCount',
label: 'artifact-$loadCount.txt',
relativePath: 'artifact-$loadCount.txt',
kind: AssistantArtifactEntryKind.file,
mimeType: 'text/plain',
previewable: true,
workspacePath: '/tmp/thread',
),
],
);
}
await tester.pumpWidget(
_buildTestApp(artifactSyncAtMs: 1, loadSnapshot: loadSnapshot),
);
await tester.pumpAndSettle();
expect(loadCount, 1);
expect(find.text('artifact-1.txt'), findsAtLeastNWidgets(1));
await tester.pumpWidget(
_buildTestApp(artifactSyncAtMs: 2, loadSnapshot: loadSnapshot),
);
await tester.pumpAndSettle();
expect(loadCount, 2);
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
});
testWidgets('keeps polling partial artifact snapshots', (tester) async {
var loadCount = 0;
await tester.pumpWidget(
_buildTestApp(
artifactSyncAtMs: 1,
artifactSyncStatus: 'partial',
loadSnapshot: () async {
loadCount += 1;
return AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
fileEntries: <AssistantArtifactEntry>[
AssistantArtifactEntry(
id: 'entry-$loadCount',
label: 'artifact-$loadCount.txt',
relativePath: 'artifact-$loadCount.txt',
kind: AssistantArtifactEntryKind.file,
mimeType: 'text/plain',
previewable: true,
workspacePath: '/tmp/thread',
),
],
);
},
),
);
await tester.pump();
expect(loadCount, 1);
await tester.pump(const Duration(milliseconds: 3100));
await tester.pump();
expect(loadCount, greaterThanOrEqualTo(2));
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
await tester.pumpWidget(const SizedBox.shrink());
});
testWidgets('explains OpenClaw runs with no exported artifacts', (
tester,
) async {
await tester.pumpWidget(
_buildTestApp(
artifactSyncAtMs: 1,
artifactSyncStatus: 'failed',
loadSnapshot: () async => const AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
filesMessage: 'No files found in the recorded working directory.',
),
),
);
await tester.pumpAndSettle();
expect(
find.text('本轮没有检测到实际生成的文件。请重新执行,并要求 OpenClaw 在当前 workspace 中创建文件。'),
findsOneWidget,
);
expect(find.textContaining('口头下载声明'), findsNothing);
expect(find.textContaining('已阻止'), findsNothing);
expect(find.textContaining('artifacts 面板'), findsNothing);
});
testWidgets('keeps the ordinary empty directory message', (tester) async {
await tester.pumpWidget(
_buildTestApp(
artifactSyncAtMs: 1,
loadSnapshot: () async => const AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
filesMessage: 'No files found in the recorded working directory.',
),
),
);
await tester.pumpAndSettle();
expect(
find.text('No files found in the recorded working directory.'),
findsOneWidget,
);
expect(
find.text('本轮没有检测到实际生成的文件。请重新执行,并要求 OpenClaw 在当前 workspace 中创建文件。'),
findsNothing,
);
});
testWidgets('keeps binary artifacts out of preview flow', (tester) async {
var previewLoadCount = 0;
await tester.pumpWidget(
_buildTestApp(
artifactSyncAtMs: 1,
loadSnapshot: () async => AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
fileEntries: <AssistantArtifactEntry>[
const AssistantArtifactEntry(
id: 'pdf',
label: 'report.pdf',
relativePath: 'report.pdf',
kind: AssistantArtifactEntryKind.file,
mimeType: 'application/pdf',
previewable: false,
workspacePath: '/tmp/thread',
),
const AssistantArtifactEntry(
id: 'md',
label: 'notes.md',
relativePath: 'notes.md',
kind: AssistantArtifactEntryKind.file,
mimeType: 'text/markdown',
previewable: true,
workspacePath: '/tmp/thread',
),
],
),
loadPreview: (_) async {
previewLoadCount += 1;
return const AssistantArtifactPreview(
kind: AssistantArtifactPreviewKind.markdown,
content: '# Notes',
);
},
),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('assistant-artifact-entry-report.pdf')),
);
await tester.pumpAndSettle();
expect(previewLoadCount, 0);
expect(
find.byKey(const Key('assistant-artifact-preview-markdown')),
findsNothing,
);
await tester.tap(
find.byKey(const ValueKey<String>('assistant-artifact-entry-notes.md')),
);
await tester.pumpAndSettle();
expect(previewLoadCount, 1);
expect(
find.byKey(const Key('assistant-artifact-preview-markdown')),
findsOneWidget,
);
});
testWidgets('opens the selected artifact location from the file list', (
tester,
) async {
AssistantArtifactEntry? openedEntry;
await tester.pumpWidget(
_buildTestApp(
artifactSyncAtMs: 1,
loadSnapshot: () async => const AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
fileEntries: <AssistantArtifactEntry>[
AssistantArtifactEntry(
id: 'pdf',
label: 'report.pdf',
relativePath: 'reports/report.pdf',
kind: AssistantArtifactEntryKind.file,
mimeType: 'application/pdf',
previewable: false,
workspacePath: '/tmp/thread',
),
],
),
onOpenEntryLocation: (entry) async {
openedEntry = entry;
},
),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(
const ValueKey<String>(
'assistant-artifact-open-location-reports/report.pdf',
),
),
);
await tester.pumpAndSettle();
expect(openedEntry?.relativePath, 'reports/report.pdf');
});
}
Widget _buildTestApp({
required double artifactSyncAtMs,
String artifactSyncStatus = '',
required Future<AssistantArtifactSnapshot> Function() loadSnapshot,
Future<AssistantArtifactPreview> Function(AssistantArtifactEntry entry)?
loadPreview,
Future<void> Function(AssistantArtifactEntry entry)? onOpenEntryLocation,
}) {
return MaterialApp(
theme: AppTheme.light(),
home: Material(
child: SizedBox(
width: 460,
height: 640,
child: AssistantArtifactSidebar(
sessionKey: 'unit-fixture-task-a',
threadTitle: 'Thread',
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
artifactSyncAtMs: artifactSyncAtMs,
artifactSyncStatus: artifactSyncStatus,
taskContextMessageCount: 2,
taskContextSelectedSkillKeys: const <String>['openclaw'],
taskContextRemoteWorkingDirectory:
'/home/ubuntu/.openclaw/workspace/tasks/unit/run',
taskContextOpenClawRunId: 'run',
taskContextOpenClawStatus: 'syncing-artifacts',
onCollapse: () {},
loadSnapshot: loadSnapshot,
loadPreview:
loadPreview ??
(_) async => const AssistantArtifactPreview.empty(),
onOpenEntryLocation: onOpenEntryLocation,
),
),
),
);
}