fix: isolate task artifacts by current sync
This commit is contained in:
parent
104486a870
commit
564fa533ac
@ -729,6 +729,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
normalizedSessionKey,
|
||||
lastArtifactSyncAtMs: syncedAtMs,
|
||||
lastArtifactSyncStatus: 'syncing',
|
||||
lastTaskArtifactRelativePaths: const <String>[],
|
||||
updatedAtMs: syncedAtMs,
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
@ -762,6 +763,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
var wroteArtifact = false;
|
||||
var failedArtifact = false;
|
||||
var skippedArtifact = false;
|
||||
final currentTaskArtifactRelativePaths = <String>[];
|
||||
for (final artifact in artifacts) {
|
||||
final relativePath = _sanitizeArtifactRelativePathInternal(
|
||||
artifact.relativePath,
|
||||
@ -791,6 +793,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
continue;
|
||||
}
|
||||
wroteArtifact = true;
|
||||
final writtenRelativePath =
|
||||
DesktopThreadArtifactService.relativePathInternal(
|
||||
root.path,
|
||||
target.path,
|
||||
);
|
||||
if (writtenRelativePath != null && writtenRelativePath.isNotEmpty) {
|
||||
currentTaskArtifactRelativePaths.add(writtenRelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
final syncStatus = wroteArtifact
|
||||
@ -802,6 +812,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
normalizedSessionKey,
|
||||
lastArtifactSyncAtMs: syncedAtMs,
|
||||
lastArtifactSyncStatus: syncStatus,
|
||||
lastTaskArtifactRelativePaths: currentTaskArtifactRelativePaths,
|
||||
updatedAtMs: syncedAtMs,
|
||||
);
|
||||
}
|
||||
|
||||
@ -242,6 +242,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||
double? lastArtifactSyncAtMs,
|
||||
String? lastArtifactSyncStatus,
|
||||
List<String>? lastTaskArtifactRelativePaths,
|
||||
}) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
@ -359,6 +360,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
lastRemoteWorkspaceRefKind: null,
|
||||
lastArtifactSyncAtMs: null,
|
||||
lastArtifactSyncStatus: null,
|
||||
lastTaskArtifactRelativePaths: const <String>[],
|
||||
))
|
||||
.copyWith(
|
||||
messages: nextMessages,
|
||||
@ -382,6 +384,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
||||
lastArtifactSyncStatus: lastArtifactSyncStatus,
|
||||
lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths,
|
||||
);
|
||||
final nextStatus =
|
||||
lifecycleStatus ??
|
||||
|
||||
@ -223,9 +223,7 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
?.selectedSkillKeys ??
|
||||
const <String>[];
|
||||
final availableKeys = skills.map((item) => item.skillKey).toSet();
|
||||
return selected
|
||||
.where(availableKeys.contains)
|
||||
.toList(growable: false);
|
||||
return selected.where(availableKeys.contains).toList(growable: false);
|
||||
}
|
||||
|
||||
String assistantModelForSession(String sessionKey) {
|
||||
@ -327,9 +325,12 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
final resolvedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey ?? currentSessionKey,
|
||||
);
|
||||
final thread = taskThreadForSessionInternal(resolvedSessionKey);
|
||||
return threadArtifactServiceInternal.loadSnapshot(
|
||||
workspacePath: assistantWorkspacePathForSession(resolvedSessionKey),
|
||||
workspaceKind: assistantWorkspaceKindForSession(resolvedSessionKey),
|
||||
artifactRelativePaths:
|
||||
thread?.lastTaskArtifactRelativePaths ?? const <String>[],
|
||||
);
|
||||
}
|
||||
|
||||
@ -340,10 +341,13 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
final resolvedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey ?? currentSessionKey,
|
||||
);
|
||||
final thread = taskThreadForSessionInternal(resolvedSessionKey);
|
||||
return threadArtifactServiceInternal.loadPreview(
|
||||
entry: entry,
|
||||
workspacePath: assistantWorkspacePathForSession(resolvedSessionKey),
|
||||
workspaceKind: assistantWorkspaceKindForSession(resolvedSessionKey),
|
||||
artifactRelativePaths:
|
||||
thread?.lastTaskArtifactRelativePaths ?? const <String>[],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ class DesktopThreadArtifactService {
|
||||
Future<AssistantArtifactSnapshot> loadSnapshot({
|
||||
required String workspacePath,
|
||||
required WorkspaceRefKind workspaceKind,
|
||||
List<String> artifactRelativePaths = const <String>[],
|
||||
}) async {
|
||||
final normalizedRef = workspacePath.trim();
|
||||
if (normalizedRef.isEmpty) {
|
||||
@ -56,9 +57,24 @@ class DesktopThreadArtifactService {
|
||||
);
|
||||
}
|
||||
|
||||
final files = await collectFilesInternal(root);
|
||||
final taskArtifactPaths = normalizeTaskArtifactPathsInternal(
|
||||
artifactRelativePaths,
|
||||
);
|
||||
final files = taskArtifactPaths.isEmpty
|
||||
? const <File>[]
|
||||
: await collectTaskArtifactFilesInternal(
|
||||
root,
|
||||
normalizedRef,
|
||||
taskArtifactPaths,
|
||||
);
|
||||
final fileEntries = await buildEntriesInternal(files, normalizedRef);
|
||||
final changes = await readGitChangesInternal(root, normalizedRef);
|
||||
final changes = taskArtifactPaths.isEmpty
|
||||
? const <AssistantArtifactChangeEntry>[]
|
||||
: await readGitChangesInternal(
|
||||
root,
|
||||
normalizedRef,
|
||||
artifactRelativePaths: taskArtifactPaths,
|
||||
);
|
||||
final results = await buildResultEntriesInternal(
|
||||
changes: changes,
|
||||
fileEntries: fileEntries,
|
||||
@ -93,6 +109,7 @@ class DesktopThreadArtifactService {
|
||||
required AssistantArtifactEntry entry,
|
||||
required String workspacePath,
|
||||
required WorkspaceRefKind workspaceKind,
|
||||
List<String> artifactRelativePaths = const <String>[],
|
||||
}) async {
|
||||
if (workspaceKind != WorkspaceRefKind.localPath) {
|
||||
return const AssistantArtifactPreview.empty(
|
||||
@ -106,9 +123,19 @@ class DesktopThreadArtifactService {
|
||||
'The recorded working directory is not available on this machine.',
|
||||
);
|
||||
}
|
||||
final taskArtifactPaths = normalizeTaskArtifactPathsInternal(
|
||||
artifactRelativePaths,
|
||||
);
|
||||
final entryRelativePath = normalizeArtifactPathInternal(entry.relativePath);
|
||||
if (entryRelativePath.isEmpty ||
|
||||
!taskArtifactPaths.contains(entryRelativePath)) {
|
||||
return const AssistantArtifactPreview.empty(
|
||||
message: 'The selected file is not part of the current task artifacts.',
|
||||
);
|
||||
}
|
||||
final targetPath = resolveAbsolutePathInternal(
|
||||
workspacePath,
|
||||
entry.relativePath,
|
||||
entryRelativePath,
|
||||
);
|
||||
final file = File(targetPath);
|
||||
if (!await file.exists()) {
|
||||
@ -117,8 +144,15 @@ class DesktopThreadArtifactService {
|
||||
'The selected file is no longer available: ${entry.relativePath}',
|
||||
);
|
||||
}
|
||||
final resolvedRelativePath = relativePathInternal(workspacePath, file.path);
|
||||
if (resolvedRelativePath == null ||
|
||||
resolvedRelativePath != entryRelativePath) {
|
||||
return const AssistantArtifactPreview.empty(
|
||||
message: 'The selected file is not part of the current task artifacts.',
|
||||
);
|
||||
}
|
||||
|
||||
final extension = fileExtensionInternal(entry.relativePath);
|
||||
final extension = fileExtensionInternal(entryRelativePath);
|
||||
final content = await file.readAsString();
|
||||
final title = entry.label;
|
||||
if (extension == 'md' || extension == 'markdown') {
|
||||
@ -171,6 +205,33 @@ class DesktopThreadArtifactService {
|
||||
return files;
|
||||
}
|
||||
|
||||
Future<List<File>> collectTaskArtifactFilesInternal(
|
||||
Directory root,
|
||||
String workspacePath,
|
||||
List<String> artifactRelativePaths,
|
||||
) async {
|
||||
final files = <File>[];
|
||||
for (final relativePath in artifactRelativePaths) {
|
||||
final target = File(resolveAbsolutePathInternal(root.path, relativePath));
|
||||
try {
|
||||
if (!await target.exists()) {
|
||||
continue;
|
||||
}
|
||||
final resolvedRelativePath = relativePathInternal(
|
||||
workspacePath,
|
||||
target.path,
|
||||
);
|
||||
if (resolvedRelativePath == null || resolvedRelativePath.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
files.add(target);
|
||||
} on FileSystemException {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
Future<List<AssistantArtifactEntry>> buildEntriesInternal(
|
||||
List<File> files,
|
||||
String workspacePath,
|
||||
@ -232,8 +293,12 @@ class DesktopThreadArtifactService {
|
||||
|
||||
Future<List<AssistantArtifactChangeEntry>> readGitChangesInternal(
|
||||
Directory workspaceRoot,
|
||||
String workspacePath,
|
||||
) async {
|
||||
String workspacePath, {
|
||||
List<String> artifactRelativePaths = const <String>[],
|
||||
}) async {
|
||||
final allowedPaths = normalizeTaskArtifactPathsInternal(
|
||||
artifactRelativePaths,
|
||||
).toSet();
|
||||
String? repositoryRoot;
|
||||
try {
|
||||
final revParse = await Process.run('git', <String>[
|
||||
@ -279,6 +344,9 @@ class DesktopThreadArtifactService {
|
||||
if (relativePath == null || relativePath.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (allowedPaths.isNotEmpty && !allowedPaths.contains(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
items.add(
|
||||
AssistantArtifactChangeEntry(
|
||||
path: relativePath,
|
||||
@ -369,6 +437,39 @@ class DesktopThreadArtifactService {
|
||||
return normalizedPath.substring(prefix.length);
|
||||
}
|
||||
|
||||
static List<String> normalizeTaskArtifactPathsInternal(
|
||||
List<String> relativePaths,
|
||||
) {
|
||||
final seen = <String>{};
|
||||
final normalized = <String>[];
|
||||
for (final relativePath in relativePaths) {
|
||||
final item = normalizeArtifactPathInternal(relativePath);
|
||||
if (item.isEmpty || !seen.add(item)) {
|
||||
continue;
|
||||
}
|
||||
normalized.add(item);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
static String normalizeArtifactPathInternal(String relativePath) {
|
||||
final trimmed = relativePath.trim().replaceAll('\\', '/');
|
||||
if (trimmed.isEmpty ||
|
||||
trimmed.startsWith('/') ||
|
||||
trimmed.startsWith('~') ||
|
||||
trimmed.contains(':')) {
|
||||
return '';
|
||||
}
|
||||
final segments = trimmed
|
||||
.split('/')
|
||||
.where((segment) => segment.isNotEmpty && segment != '.')
|
||||
.toList(growable: false);
|
||||
if (segments.isEmpty || segments.any((segment) => segment == '..')) {
|
||||
return '';
|
||||
}
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
static String normalizePathInternal(String path) {
|
||||
try {
|
||||
final type = FileSystemEntity.typeSync(path, followLinks: true);
|
||||
|
||||
@ -741,6 +741,7 @@ class ThreadContextState {
|
||||
this.lastRemoteWorkspaceRefKind,
|
||||
this.lastArtifactSyncAtMs,
|
||||
this.lastArtifactSyncStatus,
|
||||
this.lastTaskArtifactRelativePaths = const <String>[],
|
||||
});
|
||||
|
||||
final List<GatewayChatMessage> messages;
|
||||
@ -758,6 +759,7 @@ class ThreadContextState {
|
||||
final WorkspaceRefKind? lastRemoteWorkspaceRefKind;
|
||||
final double? lastArtifactSyncAtMs;
|
||||
final String? lastArtifactSyncStatus;
|
||||
final List<String> lastTaskArtifactRelativePaths;
|
||||
|
||||
ThreadContextState copyWith({
|
||||
List<GatewayChatMessage>? messages,
|
||||
@ -776,6 +778,7 @@ class ThreadContextState {
|
||||
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||
double? lastArtifactSyncAtMs,
|
||||
String? lastArtifactSyncStatus,
|
||||
List<String>? lastTaskArtifactRelativePaths,
|
||||
}) {
|
||||
return ThreadContextState(
|
||||
messages: messages ?? this.messages,
|
||||
@ -800,6 +803,9 @@ class ThreadContextState {
|
||||
lastArtifactSyncAtMs: lastArtifactSyncAtMs ?? this.lastArtifactSyncAtMs,
|
||||
lastArtifactSyncStatus:
|
||||
lastArtifactSyncStatus ?? this.lastArtifactSyncStatus,
|
||||
lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths == null
|
||||
? this.lastTaskArtifactRelativePaths
|
||||
: _stringListFromJson(lastTaskArtifactRelativePaths),
|
||||
);
|
||||
}
|
||||
|
||||
@ -822,6 +828,7 @@ class ThreadContextState {
|
||||
'lastRemoteWorkspaceRefKind': lastRemoteWorkspaceRefKind?.name,
|
||||
'lastArtifactSyncAtMs': lastArtifactSyncAtMs,
|
||||
'lastArtifactSyncStatus': lastArtifactSyncStatus,
|
||||
'lastTaskArtifactRelativePaths': lastTaskArtifactRelativePaths,
|
||||
};
|
||||
}
|
||||
|
||||
@ -897,10 +904,34 @@ class ThreadContextState {
|
||||
})(),
|
||||
lastArtifactSyncAtMs: asDouble(json['lastArtifactSyncAtMs']),
|
||||
lastArtifactSyncStatus: json['lastArtifactSyncStatus']?.toString(),
|
||||
lastTaskArtifactRelativePaths: _stringListFromJson(
|
||||
json['lastTaskArtifactRelativePaths'],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _stringListFromJson(Object? value) {
|
||||
if (value is! List) {
|
||||
return const <String>[];
|
||||
}
|
||||
final seen = <String>{};
|
||||
final items = <String>[];
|
||||
for (final item in value) {
|
||||
final normalized = item?.toString().trim().replaceAll('\\', '/') ?? '';
|
||||
if (normalized.isEmpty || normalized.startsWith('/')) {
|
||||
continue;
|
||||
}
|
||||
if (normalized.split('/').any((segment) => segment == '..')) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(normalized)) {
|
||||
items.add(normalized);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
class ThreadLifecycleState {
|
||||
const ThreadLifecycleState({
|
||||
required this.archived,
|
||||
@ -984,6 +1015,7 @@ class TaskThread {
|
||||
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||
double? lastArtifactSyncAtMs,
|
||||
String? lastArtifactSyncStatus,
|
||||
List<String>? lastTaskArtifactRelativePaths,
|
||||
}) : threadId = _resolveThreadId(threadId),
|
||||
title = title ?? '',
|
||||
ownerScope =
|
||||
@ -1029,6 +1061,9 @@ class TaskThread {
|
||||
lastArtifactSyncStatus?.trim().isNotEmpty == true
|
||||
? lastArtifactSyncStatus!.trim()
|
||||
: null,
|
||||
lastTaskArtifactRelativePaths: _stringListFromJson(
|
||||
lastTaskArtifactRelativePaths,
|
||||
),
|
||||
),
|
||||
lifecycleState =
|
||||
lifecycleState ??
|
||||
@ -1067,6 +1102,8 @@ class TaskThread {
|
||||
contextState.lastRemoteWorkspaceRefKind;
|
||||
double? get lastArtifactSyncAtMs => contextState.lastArtifactSyncAtMs;
|
||||
String? get lastArtifactSyncStatus => contextState.lastArtifactSyncStatus;
|
||||
List<String> get lastTaskArtifactRelativePaths =>
|
||||
contextState.lastTaskArtifactRelativePaths;
|
||||
String get latestResolvedRuntimeModel =>
|
||||
contextState.latestResolvedRuntimeModel;
|
||||
String get latestResolvedProviderId => contextState.latestResolvedProviderId;
|
||||
@ -1110,6 +1147,7 @@ class TaskThread {
|
||||
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||
double? lastArtifactSyncAtMs,
|
||||
String? lastArtifactSyncStatus,
|
||||
List<String>? lastTaskArtifactRelativePaths,
|
||||
}) {
|
||||
return TaskThread(
|
||||
threadId: threadId ?? this.threadId,
|
||||
@ -1133,6 +1171,7 @@ class TaskThread {
|
||||
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||
lastArtifactSyncAtMs: lastArtifactSyncAtMs,
|
||||
lastArtifactSyncStatus: lastArtifactSyncStatus,
|
||||
lastTaskArtifactRelativePaths: lastTaskArtifactRelativePaths,
|
||||
),
|
||||
lifecycleState: (lifecycleState ?? this.lifecycleState).copyWith(
|
||||
archived: archived,
|
||||
@ -1249,6 +1288,7 @@ class TaskThread {
|
||||
'lastRemoteWorkspaceRefKind': json['lastRemoteWorkspaceRefKind'],
|
||||
'lastArtifactSyncAtMs': json['lastArtifactSyncAtMs'],
|
||||
'lastArtifactSyncStatus': json['lastArtifactSyncStatus'],
|
||||
'lastTaskArtifactRelativePaths': json['lastTaskArtifactRelativePaths'],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_runtime_coordination_impl.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_thread_binding.dart';
|
||||
import 'package:xworkmate/runtime/assistant_artifacts.dart';
|
||||
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
@ -196,14 +197,9 @@ void main() {
|
||||
final snapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: 'session-1',
|
||||
);
|
||||
expect(
|
||||
snapshot.fileEntries.map((entry) => entry.relativePath),
|
||||
contains('notes/hello.txt'),
|
||||
);
|
||||
expect(
|
||||
snapshot.fileEntries.map((entry) => entry.relativePath),
|
||||
contains('notes/hello.v2.txt'),
|
||||
);
|
||||
expect(snapshot.fileEntries.map((entry) => entry.relativePath), <String>[
|
||||
'notes/hello.v2.txt',
|
||||
]);
|
||||
expect(
|
||||
controller
|
||||
.requireTaskThreadForSessionInternal('session-1')
|
||||
@ -212,6 +208,83 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'limits the artifact panel to files produced by the current task sync',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-isolated-artifact-workspace-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localWorkspace.exists()) {
|
||||
await localWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final staleArtifact = File('${localWorkspace.path}/old-task-report.md');
|
||||
await staleArtifact.writeAsString('stale task output');
|
||||
|
||||
controller.upsertTaskThreadInternal(
|
||||
'session-1',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'session-1',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: localWorkspace.path,
|
||||
displayPath: localWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
);
|
||||
|
||||
final result = GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'hello',
|
||||
turnId: 'turn-2',
|
||||
raw: <String, dynamic>{
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'current-task-report.md',
|
||||
'content': 'current task output',
|
||||
'contentType': 'text/markdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
);
|
||||
|
||||
await controller.persistGoTaskArtifactsForSessionInternal(
|
||||
'session-1',
|
||||
result,
|
||||
);
|
||||
|
||||
final snapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: 'session-1',
|
||||
);
|
||||
final relativePaths = snapshot.fileEntries
|
||||
.map((entry) => entry.relativePath)
|
||||
.toList(growable: false);
|
||||
expect(relativePaths, <String>['current-task-report.md']);
|
||||
|
||||
final stalePreview = await controller.loadAssistantArtifactPreview(
|
||||
AssistantArtifactEntry(
|
||||
id: '${localWorkspace.path}::old-task-report.md',
|
||||
label: 'old-task-report.md',
|
||||
relativePath: 'old-task-report.md',
|
||||
kind: AssistantArtifactEntryKind.file,
|
||||
mimeType: 'text/markdown',
|
||||
previewable: true,
|
||||
workspacePath: localWorkspace.path,
|
||||
),
|
||||
sessionKey: 'session-1',
|
||||
);
|
||||
expect(stalePreview.kind, AssistantArtifactPreviewKind.empty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'downloads bridge URL artifacts into the local thread workspace',
|
||||
() async {
|
||||
@ -895,6 +968,8 @@ void main() {
|
||||
await localWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final staleArtifact = File('${localWorkspace.path}/old-task-report.md');
|
||||
await staleArtifact.writeAsString('stale task output');
|
||||
controller.upsertTaskThreadInternal(
|
||||
'session-1',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
@ -927,6 +1002,10 @@ void main() {
|
||||
.lastArtifactSyncStatus,
|
||||
'no-artifacts',
|
||||
);
|
||||
final snapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: 'session-1',
|
||||
);
|
||||
expect(snapshot.fileEntries, isEmpty);
|
||||
});
|
||||
|
||||
test('skips download URL artifacts outside the bridge host', () async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user