fix: surface queued task feedback
This commit is contained in:
parent
484b10ecf5
commit
208545dd71
@ -76,5 +76,6 @@ All download buttons currently point to the latest GitHub release page.
|
||||
- [Changelog](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/releases/xworkmate-changelog.md)
|
||||
- [Feature Matrix](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/plans/xworkmate-ui-feature-matrix.md)
|
||||
- [Roadmap](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/plans/xworkmate-ui-feature-roadmap.md)
|
||||
- [Cross-Repo Task State Workflow](docs/architecture/cross-repo-task-state-workflow.md)
|
||||
- [Gateway Dev Runbook](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/runbooks/gateway-dev-runbook.md)
|
||||
- [Security Rules](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/security/secure-development-rules.md)
|
||||
|
||||
196
docs/architecture/cross-repo-task-state-workflow.md
Normal file
196
docs/architecture/cross-repo-task-state-workflow.md
Normal file
@ -0,0 +1,196 @@
|
||||
# Cross-Repo Task State Workflow
|
||||
|
||||
This document records the task-state workflow across:
|
||||
|
||||
- `xworkmate-app`
|
||||
- `xworkmate-bridge`
|
||||
- `openclaw-multi-session-plugins`
|
||||
|
||||
The core ownership split is:
|
||||
|
||||
- `xworkmate-app` owns task UI state, `TaskThread` persistence, local thread workspaces, and the OpenClaw submit queue.
|
||||
- `xworkmate-bridge` owns public session/routing contracts, provider compatibility, normalized results, and OpenClaw task-submit routing.
|
||||
- `openclaw-multi-session-plugins` owns OpenClaw task-scoped artifact preparation, export, and artifact-scope isolation.
|
||||
|
||||
## Overall Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["User input / follow-up"] --> APP["xworkmate-app<br/>TaskThread + UI state"]
|
||||
APP -->|session.start / session.message| BR["xworkmate-bridge<br/>/acp/rpc or /gateway/openclaw"]
|
||||
BR -->|xworkmate.routing.resolve| ROUTE{"Execution target"}
|
||||
ROUTE -->|single-agent| AG["codex / opencode / gemini / hermes"]
|
||||
ROUTE -->|gateway=openclaw| OC["OpenClaw Gateway Runtime"]
|
||||
OC --> PLUG["openclaw-multi-session-plugins<br/>task-scoped artifacts"]
|
||||
PLUG -->|artifactRef / files / downloadUrl| BR
|
||||
BR -->|normalized result / SSE update| APP
|
||||
APP -->|write files| LOCAL["$HOME/.xworkmate/threads/<session>"]
|
||||
APP -->|persist index| STORE["~/Library/Application Support/xworkmate/tasks/threads.json"]
|
||||
```
|
||||
|
||||
## App TaskThread State Machine
|
||||
|
||||
`TaskThread.lifecycleState.status` is intentionally small. Most terminal outcomes return to `ready`; the specific terminal result is stored in `lastResultCode`.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Ready: create task / restore persisted task
|
||||
|
||||
Ready --> Queued: OpenClaw gateway active slot is full
|
||||
Queued --> Running: drainOpenClawGatewayQueue
|
||||
Queued --> Ready: abortRun\nlastResultCode=aborted
|
||||
Queued --> Ready: queue full\nlastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL
|
||||
|
||||
Ready --> Running: sendChatMessage\nmarkGatewayChatRun
|
||||
Running --> Ready: GoTaskServiceResult.success=true\nlastResultCode=success
|
||||
Running --> Ready: result.status / result.code\nlastResultCode=<status|code>
|
||||
Running --> Ready: no displayable output\nlastResultCode=failed
|
||||
Running --> Ready: ACP interrupted / timeout\nlastResultCode=ACP_*
|
||||
Running --> Ready: abortRun\nlastResultCode=aborted
|
||||
|
||||
Ready --> Archived: user archives task
|
||||
Archived --> Ready: user restores task
|
||||
|
||||
note right of Ready
|
||||
lifecycleStatus terminal state is usually ready.
|
||||
Read lastResultCode for result detail:
|
||||
success / failed / error / aborted /
|
||||
artifact_missing / ACP_HTTP_*
|
||||
end note
|
||||
```
|
||||
|
||||
## Task Workspace Context Injection
|
||||
|
||||
Every app-owned task has a local workspace under `$HOME/.xworkmate/threads/<session>`. For remote execution, the bridge/runtime may also resolve a remote task workspace hint. The app passes the task workspace in two ways:
|
||||
|
||||
- Structured request fields: `workingDirectory` and, when available, `remoteWorkingDirectoryHint`.
|
||||
- External conversation context: a `TaskThread workspace context` prefix is added to the prompt sent to Bridge/OpenClaw.
|
||||
|
||||
The local chat transcript still stores the user's original text. Only the external task prompt is enriched, so the UI does not show internal workspace rules as user content.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as "XWorkmate UI"
|
||||
participant APP as "TaskThread runtime"
|
||||
participant BR as "Bridge session"
|
||||
participant OC as "OpenClaw / provider"
|
||||
participant WS as "Task workspace"
|
||||
|
||||
UI->>APP: "user message"
|
||||
APP->>WS: "ensure $HOME/.xworkmate/threads/<session>"
|
||||
APP->>APP: "build TaskThread workspace context"
|
||||
APP->>BR: "taskPrompt + workingDirectory + remoteWorkingDirectoryHint"
|
||||
BR->>OC: "session.start / session.message"
|
||||
OC->>WS: "final files must be exported into task scope"
|
||||
BR-->>APP: "result + artifact refs"
|
||||
APP->>WS: "sync inline/downloaded artifacts"
|
||||
```
|
||||
|
||||
Prompt-level workspace rules are deliberately strict. `remoteWorkingDirectoryHint` is the writable task workspace for remote OpenClaw/provider execution when present; otherwise `workingDirectory` is used.
|
||||
|
||||
- Treat the current task workspace as the only writable workspace for the task execution.
|
||||
- Create, modify, and export task files inside that workspace or its task artifact scope.
|
||||
- Do not use global OpenClaw media/cache paths, `/tmp`, Downloads, Desktop, or other arbitrary directories as final deliverable locations.
|
||||
- If a tool creates output outside the task workspace, copy/export the final deliverables into the task workspace before claiming completion.
|
||||
- Prefer local task-workspace paths, or paths relative to that workspace, when reporting files back to the user.
|
||||
|
||||
## OpenClaw Gateway Queue
|
||||
|
||||
The app serializes OpenClaw gateway execution locally because OpenClaw task execution is treated as a constrained gateway lane.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["APP selects Gateway + OpenClaw"] --> B{"active < 1 ?"}
|
||||
B -->|yes| C["Run immediately<br/>lifecycleStatus=running"]
|
||||
B -->|no| D{"queue < 20 ?"}
|
||||
D -->|yes| E["Enqueue<br/>lifecycleStatus=queued<br/>lastArtifactSyncStatus=queued"]
|
||||
D -->|no| F["Queue full<br/>lastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL"]
|
||||
|
||||
E --> G["drain queue"]
|
||||
G --> C
|
||||
C --> H["Bridge /gateway/openclaw"]
|
||||
H --> I["OpenClaw execution"]
|
||||
I --> J{"Result"}
|
||||
J -->|success + output/files| K["APP ready<br/>lastResultCode=success<br/>sync artifacts"]
|
||||
J -->|artifact guard| L["APP ready<br/>lastResultCode=artifact_missing"]
|
||||
J -->|failure/interruption| M["APP ready<br/>lastResultCode=failed/error/ACP_*"]
|
||||
```
|
||||
|
||||
## Bridge Session And Routing Workflow
|
||||
|
||||
The bridge exposes one public session contract while keeping provider-specific behavior behind compatibility layers. `/gateway/openclaw` is a narrow OpenClaw task-submit lane, not a general ACP base endpoint.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ["APP request"] --> EP{"HTTP / WS entry"}
|
||||
EP -->|/acp or /acp/rpc| RPC["General JSON-RPC"]
|
||||
EP -->|/gateway/openclaw| GW["OpenClaw task-submit endpoint"]
|
||||
|
||||
RPC --> METHOD{"method"}
|
||||
METHOD -->|acp.capabilities| CAP["Return agent providers + gatewayProviders=openclaw"]
|
||||
METHOD -->|xworkmate.routing.resolve| ROUTE["Resolve single-agent / gateway"]
|
||||
METHOD -->|session.start| START["Create / start session turn"]
|
||||
METHOD -->|session.message| MSG["Continue existing session"]
|
||||
METHOD -->|session.cancel / session.close| CTRL["Cancel / close session"]
|
||||
|
||||
GW --> GUARD{"method is session.start / session.message ?"}
|
||||
GUARD -->|no| REJECT["Reject: not a global ACP base"]
|
||||
GUARD -->|yes| FORCE["Force routing=gateway/openclaw<br/>Reject multiAgent"]
|
||||
FORCE --> START
|
||||
|
||||
START --> ORCH["session_orchestrator"]
|
||||
MSG --> ORCH
|
||||
ORCH --> PROVIDER{"provider compat"}
|
||||
PROVIDER -->|single-agent| SA["codex / opencode / gemini / hermes"]
|
||||
PROVIDER -->|gateway| OCG["OpenClaw runtime"]
|
||||
PROVIDER -->|multi-agent via /acp/rpc| MA["multi-agent orchestration"]
|
||||
|
||||
SA --> NORM["normalized result"]
|
||||
OCG --> NORM
|
||||
MA --> NORM
|
||||
NORM --> OUT["success / status / turnId / output / artifacts / resolved*"]
|
||||
```
|
||||
|
||||
## OpenClaw Plugin Artifact Scope
|
||||
|
||||
OpenClaw artifacts are scoped by task session and run. This prevents one task or turn from borrowing files from another.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CTX["OpenClaw plugin context<br/>sessionKey + runId + workspaceDir"] --> PREP["prepareXWorkmateArtifacts"]
|
||||
PREP --> SCOPE["artifactScope = tasks/<sessionKey>/<runId>"]
|
||||
SCOPE --> DIR["artifactDirectory"]
|
||||
DIR --> RUN["OpenClaw writes files"]
|
||||
RUN --> EXPORT["exportXWorkmateArtifacts"]
|
||||
EXPORT --> VALIDATE{"scope matches sessionKey/runId ?"}
|
||||
VALIDATE -->|no| ERR["Reject cross-task / cross-run artifact"]
|
||||
VALIDATE -->|yes| MANIFEST["manifest + artifactRef + files"]
|
||||
MANIFEST --> BR["Bridge result"]
|
||||
BR --> APP["APP downloads or inlines into local thread workspace"]
|
||||
|
||||
note right of SCOPE
|
||||
Concurrent task isolation is based on:
|
||||
tasks/<session>/<run>
|
||||
Do not reuse other sessions or previous run files.
|
||||
end note
|
||||
```
|
||||
|
||||
## Status Field Mapping
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
APP1["APP TaskThread.lifecycleState.status"] --> A1["queued / running / ready / archived"]
|
||||
APP2["APP TaskThread.lifecycleState.lastResultCode"] --> A2["queued / running / success / failed / error / aborted / artifact_missing / ACP_HTTP_*"]
|
||||
APP3["APP lastArtifactSyncStatus"] --> A3["queued / running / synced / no-artifacts / failed"]
|
||||
BR1["Bridge result.status"] --> B1["available / success / failed / provider status"]
|
||||
BR2["Bridge result.success"] --> B2["true / false"]
|
||||
PL1["Plugin artifact export"] --> P1["scopeKind=task<br/>artifactScope=tasks/session/run"]
|
||||
```
|
||||
|
||||
## Boundary Rules
|
||||
|
||||
- The app does not store OpenClaw URLs. It only consumes bridge capabilities where `gatewayProviders` includes `openclaw`.
|
||||
- `/gateway/openclaw` is only for OpenClaw `session.start` and `session.message`; it is not a global ACP endpoint.
|
||||
- Follow-up conversation uses the same `sessionKey` / `threadId`. Bridge `session.message` must continue the provider session state or return a structured continuation error.
|
||||
- Artifact ownership is enforced by `openclaw-multi-session-plugins` with `tasks/<session>/<run>` scope. The app syncs only the current run's artifacts into the local thread workspace.
|
||||
- Upgrade/install flows must preserve real local history. Cleanup must only remove explicitly known test-pollution session keys.
|
||||
@ -67,6 +67,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
bool assistantSessionHasPendingRun(String sessionKey) {
|
||||
final normalized = normalizedAssistantSessionKeyInternal(sessionKey);
|
||||
return aiGatewayPendingSessionKeysInternal.contains(normalized) ||
|
||||
(openClawGatewayQueuedTurnsBySessionInternal[normalized]?.isNotEmpty ??
|
||||
false) ||
|
||||
(multiAgentRunPendingInternal &&
|
||||
matchesSessionKey(
|
||||
normalized,
|
||||
@ -417,11 +419,21 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
required String agentId,
|
||||
required Map<String, dynamic> metadata,
|
||||
required bool resumeSessionHint,
|
||||
bool appendUserTurn = true,
|
||||
}) async {
|
||||
final resumeSession =
|
||||
resumeSessionHint ||
|
||||
shouldResumeGatewaySessionForNextSendInternal(sessionKey);
|
||||
appendGatewayUserTurnInternal(sessionKey, message);
|
||||
(appendUserTurn &&
|
||||
shouldResumeGatewaySessionForNextSendInternal(sessionKey));
|
||||
final taskPrompt = taskWorkspaceContextPromptInternal(
|
||||
sessionKey: sessionKey,
|
||||
userPrompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
|
||||
);
|
||||
if (appendUserTurn) {
|
||||
appendGatewayUserTurnInternal(sessionKey, message);
|
||||
}
|
||||
markGatewayChatRunInternal(sessionKey);
|
||||
try {
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
@ -430,7 +442,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
threadId: sessionKey,
|
||||
target: target,
|
||||
provider: provider,
|
||||
prompt: message,
|
||||
prompt: taskPrompt,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
|
||||
model: model,
|
||||
@ -482,6 +494,53 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
}
|
||||
}
|
||||
|
||||
String taskWorkspaceContextPromptInternal({
|
||||
required String sessionKey,
|
||||
required String userPrompt,
|
||||
required String workingDirectory,
|
||||
required String remoteWorkingDirectoryHint,
|
||||
}) {
|
||||
final requestText = userPrompt.trim().isEmpty
|
||||
? 'See attached.'
|
||||
: userPrompt.trim();
|
||||
final buffer = StringBuffer()
|
||||
..writeln('TaskThread workspace context:')
|
||||
..writeln('- sessionKey: $sessionKey')
|
||||
..writeln('- localWorkspace: ${workingDirectory.trim()}');
|
||||
final remoteHint = remoteWorkingDirectoryHint.trim();
|
||||
if (remoteHint.isNotEmpty) {
|
||||
buffer.writeln('- remoteWorkspaceHint: $remoteHint');
|
||||
}
|
||||
buffer.writeln(
|
||||
'- currentTaskWorkspace: ${remoteHint.isNotEmpty ? remoteHint : workingDirectory.trim()}',
|
||||
);
|
||||
buffer
|
||||
..writeln()
|
||||
..writeln('Workspace isolation rules:')
|
||||
..writeln(
|
||||
'1. Treat currentTaskWorkspace as the only writable workspace for this TaskThread execution.',
|
||||
)
|
||||
..writeln(
|
||||
'2. Create, modify, and export task files inside currentTaskWorkspace or its task artifact scope.',
|
||||
)
|
||||
..writeln(
|
||||
'3. Do not use arbitrary global directories, OpenClaw media cache, Downloads, Desktop, or /tmp as final deliverable locations.',
|
||||
)
|
||||
..writeln(
|
||||
'4. If a tool creates output outside currentTaskWorkspace, copy or export the final deliverables into currentTaskWorkspace before claiming completion.',
|
||||
)
|
||||
..writeln(
|
||||
'5. When reporting files, prefer paths inside currentTaskWorkspace or paths relative to currentTaskWorkspace.',
|
||||
)
|
||||
..writeln(
|
||||
'6. The app syncs final artifacts from currentTaskWorkspace back into localWorkspace.',
|
||||
)
|
||||
..writeln()
|
||||
..writeln('User request:')
|
||||
..write(requestText);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
bool usesOpenClawGatewayQueueInternal(
|
||||
AssistantExecutionTarget target,
|
||||
SingleAgentProvider provider,
|
||||
@ -493,6 +552,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
Future<void> enqueueOpenClawGatewayTurnInternal(
|
||||
OpenClawGatewayQueuedTurnInternal turn,
|
||||
) async {
|
||||
appendGatewayUserTurnInternal(turn.sessionKey, turn.message);
|
||||
if (openClawGatewayActiveTasksInternal >=
|
||||
openClawGatewayMaxActiveTasksInternal &&
|
||||
openClawGatewayQueuedTurnsInternal.length >=
|
||||
@ -632,6 +692,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
agentId: turn.agentId,
|
||||
metadata: turn.metadata,
|
||||
resumeSessionHint: turn.resumeSessionHint,
|
||||
appendUserTurn: false,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@ -160,12 +160,18 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
||||
);
|
||||
controller.recomputeTasksInternal();
|
||||
try {
|
||||
final taskPrompt = controller.taskWorkspaceContextPromptInternal(
|
||||
sessionKey: sessionKey,
|
||||
userPrompt: composedPrompt,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint?.trim() ?? '',
|
||||
);
|
||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: controller.assistantExecutionTargetForSession(sessionKey),
|
||||
prompt: composedPrompt,
|
||||
prompt: taskPrompt,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint?.trim() ?? '',
|
||||
model: controller.assistantModelForSession(sessionKey),
|
||||
|
||||
@ -344,6 +344,9 @@ String sessionStatusInternal(
|
||||
if (session.abortedLastRun == true) {
|
||||
return 'failed';
|
||||
}
|
||||
if (normalizedLifecycle == 'queued') {
|
||||
return 'queued';
|
||||
}
|
||||
if (sessionPending) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
@ -2,7 +2,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
|
||||
enum AssistantTaskProgressPhase { idle, running, syncingArtifacts, interrupted }
|
||||
enum AssistantTaskProgressPhase {
|
||||
idle,
|
||||
queued,
|
||||
running,
|
||||
syncingArtifacts,
|
||||
interrupted,
|
||||
}
|
||||
|
||||
class AssistantTaskProgressState {
|
||||
const AssistantTaskProgressState({
|
||||
@ -26,6 +32,7 @@ class AssistantTaskProgressState {
|
||||
bool get visible => phase != AssistantTaskProgressPhase.idle;
|
||||
bool get interrupted => phase == AssistantTaskProgressPhase.interrupted;
|
||||
bool get running =>
|
||||
phase == AssistantTaskProgressPhase.queued ||
|
||||
phase == AssistantTaskProgressPhase.running ||
|
||||
phase == AssistantTaskProgressPhase.syncingArtifacts;
|
||||
}
|
||||
@ -146,6 +153,15 @@ AssistantTaskProgressState assistantTaskProgressState({
|
||||
final budget = runtimeBudgetMinutes == null || runtimeBudgetMinutes <= 0
|
||||
? null
|
||||
: runtimeBudgetMinutes;
|
||||
final result = lastResultCode.trim().toUpperCase();
|
||||
if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') {
|
||||
return AssistantTaskProgressState(
|
||||
phase: AssistantTaskProgressPhase.queued,
|
||||
label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'),
|
||||
value: 0.18,
|
||||
runtimeBudgetMinutes: budget,
|
||||
);
|
||||
}
|
||||
if (pending && syncStatus == 'syncing') {
|
||||
return AssistantTaskProgressState(
|
||||
phase: AssistantTaskProgressPhase.syncingArtifacts,
|
||||
@ -161,7 +177,6 @@ AssistantTaskProgressState assistantTaskProgressState({
|
||||
runtimeBudgetMinutes: budget,
|
||||
);
|
||||
}
|
||||
final result = lastResultCode.trim().toUpperCase();
|
||||
if (status == 'interrupted' || syncStatus == 'interrupted') {
|
||||
return AssistantTaskProgressState(
|
||||
phase: AssistantTaskProgressPhase.interrupted,
|
||||
|
||||
@ -2,9 +2,9 @@ name: xworkmate
|
||||
description: "XWorkmate desktop-first AI workspace shell."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0-beta.2+5
|
||||
build-date: 2026-05-18
|
||||
build-id: e7e1135
|
||||
version: 1.0.0-beta.2+6
|
||||
build-date: 2026-05-19
|
||||
build-id: 0f5440d
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
||||
@ -57,6 +57,32 @@ void main() {
|
||||
expect(indicator.value, 0.82);
|
||||
});
|
||||
|
||||
testWidgets('shows queued progress while a task waits to run', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
assistantTaskProgressState(
|
||||
pending: true,
|
||||
lifecycleStatus: 'queued',
|
||||
lastResultCode: 'queued',
|
||||
artifactSyncStatus: 'queued',
|
||||
),
|
||||
onStop: () {},
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('任务已排队,等待执行...'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-task-progress-stop-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
final indicator = tester.widget<LinearProgressIndicator>(
|
||||
find.byKey(const Key('assistant-task-progress-indicator')),
|
||||
);
|
||||
expect(indicator.value, 0.18);
|
||||
});
|
||||
|
||||
testWidgets('shows interrupted state after ACP connection closes', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -898,7 +898,21 @@ void main() {
|
||||
await controller.sendChatMessage('first turn');
|
||||
|
||||
expect(fakeGoTaskService.requests, hasLength(1));
|
||||
expect(fakeGoTaskService.requests.single.resumeSession, isFalse);
|
||||
final request = fakeGoTaskService.requests.single;
|
||||
expect(request.resumeSession, isFalse);
|
||||
expect(request.prompt, contains('TaskThread workspace context:'));
|
||||
expect(request.prompt, contains('- sessionKey: unit-fixture-task-a'));
|
||||
expect(request.prompt, contains(request.workingDirectory));
|
||||
expect(request.prompt, contains(request.remoteWorkingDirectoryHint));
|
||||
expect(request.prompt, contains('User request:\nfirst turn'));
|
||||
expect(
|
||||
controller.chatMessages.map((message) => message.text),
|
||||
contains('first turn'),
|
||||
);
|
||||
expect(
|
||||
controller.chatMessages.map((message) => message.text).join('\n'),
|
||||
isNot(contains('TaskThread workspace context:')),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
@ -1632,11 +1646,15 @@ void main() {
|
||||
final taskBRequest = fakeGoTaskService.requests[1];
|
||||
expect(taskARequest.sessionId, sessionA);
|
||||
expect(taskBRequest.sessionId, sessionB);
|
||||
expect(taskARequest.prompt, taskBRequest.prompt);
|
||||
expect(taskARequest.prompt, isNot(taskBRequest.prompt));
|
||||
expect(taskARequest.prompt, contains(prompt));
|
||||
expect(taskBRequest.prompt, contains(prompt));
|
||||
expect(taskARequest.resumeSession, isFalse);
|
||||
expect(taskBRequest.resumeSession, isFalse);
|
||||
expect(taskARequest.workingDirectory, endsWith('/$sessionA'));
|
||||
expect(taskBRequest.workingDirectory, endsWith('/$sessionB'));
|
||||
expect(taskARequest.prompt, contains(taskARequest.workingDirectory));
|
||||
expect(taskBRequest.prompt, contains(taskBRequest.workingDirectory));
|
||||
expect(
|
||||
taskARequest.workingDirectory,
|
||||
isNot(taskBRequest.workingDirectory),
|
||||
@ -2192,6 +2210,26 @@ void main() {
|
||||
.status,
|
||||
'queued',
|
||||
);
|
||||
expect(
|
||||
controller.assistantSessionHasPendingRun('queue-task-b'),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.assistantSessionHasPendingRun('queue-task-c'),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
controller.localSessionMessagesInternal['queue-task-b']!.map(
|
||||
(message) => message.text,
|
||||
),
|
||||
contains('same prompt'),
|
||||
);
|
||||
expect(
|
||||
controller.localSessionMessagesInternal['queue-task-c']!.map(
|
||||
(message) => message.text,
|
||||
),
|
||||
contains('different prompt'),
|
||||
);
|
||||
|
||||
fakeGoTaskService.complete(
|
||||
'queue-task-a',
|
||||
@ -2214,9 +2252,12 @@ void main() {
|
||||
|
||||
final taskBRequest = fakeGoTaskService.requests[1];
|
||||
expect(taskBRequest.sessionId, 'queue-task-b');
|
||||
expect(taskBRequest.prompt, 'same prompt');
|
||||
expect(taskBRequest.prompt, contains('TaskThread workspace context:'));
|
||||
expect(taskBRequest.prompt, contains('- sessionKey: queue-task-b'));
|
||||
expect(taskBRequest.prompt, contains('User request:\nsame prompt'));
|
||||
expect(taskBRequest.resumeSession, isFalse);
|
||||
expect(taskBRequest.workingDirectory, endsWith('/queue-task-b'));
|
||||
expect(taskBRequest.prompt, contains(taskBRequest.workingDirectory));
|
||||
expect(
|
||||
taskBRequest.remoteWorkingDirectoryHint,
|
||||
endsWith('/threads/queue-task-b'),
|
||||
@ -2239,12 +2280,25 @@ void main() {
|
||||
'queue-task-b',
|
||||
'ready',
|
||||
);
|
||||
expect(
|
||||
controller.localSessionMessagesInternal['queue-task-b']!
|
||||
.where(
|
||||
(message) =>
|
||||
message.role == 'user' && message.text == 'same prompt',
|
||||
)
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
await fakeGoTaskService.waitForRequestCount(3);
|
||||
|
||||
final taskCRequest = fakeGoTaskService.requests[2];
|
||||
expect(taskCRequest.sessionId, 'queue-task-c');
|
||||
expect(taskCRequest.prompt, 'different prompt');
|
||||
expect(
|
||||
taskCRequest.prompt,
|
||||
contains('User request:\ndifferent prompt'),
|
||||
);
|
||||
expect(taskCRequest.workingDirectory, endsWith('/queue-task-c'));
|
||||
expect(taskCRequest.prompt, contains(taskCRequest.workingDirectory));
|
||||
fakeGoTaskService.complete(
|
||||
'queue-task-c',
|
||||
const GoTaskServiceResult(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user