Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Q
qjclaw-dmg
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AI-甘富林
qjclaw-dmg
Commits
c8f6b1df
Commit
c8f6b1df
authored
May 20, 2026
by
edy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(desktop): run automation tasks in background
parent
143789ee
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
355 additions
and
13 deletions
+355
-13
ipc.ts
apps/desktop/src/main/ipc.ts
+156
-11
project-context-lifecycle.ts
apps/desktop/src/main/services/project-context-lifecycle.ts
+4
-1
automationTaskIpcSource.test.ts
apps/desktop/test/automationTaskIpcSource.test.ts
+62
-0
automationTaskService.test.ts
apps/desktop/test/automationTaskService.test.ts
+7
-1
projectContextLifecycle.test.ts
apps/desktop/test/projectContextLifecycle.test.ts
+94
-0
taskPanelService.test.ts
apps/desktop/test/taskPanelService.test.ts
+25
-0
taskPanelViewSource.test.ts
apps/ui/test/taskPanelViewSource.test.ts
+7
-0
No files found.
apps/desktop/src/main/ipc.ts
View file @
c8f6b1df
...
@@ -5,6 +5,7 @@ import path from "node:path";
...
@@ -5,6 +5,7 @@ import path from "node:path";
import
{
import
{
IPC_CHANNELS
,
IPC_CHANNELS
,
type
AppConfig
,
type
AppConfig
,
type
AutomationTask
,
type
AutomationTaskRun
,
type
AutomationTaskRun
,
type
CreateAutomationTaskInput
,
type
CreateAutomationTaskInput
,
type
ChatAttachment
,
type
ChatAttachment
,
...
@@ -1733,6 +1734,47 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -1733,6 +1734,47 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}
}
};
};
const summarizeAutomationReply = (replyText: string): string => {
const normalized = replyText.replace(/
\
s+/g, "
").trim();
return normalized.length > 160 ? normalized.slice(0, 160) + "
...
" : normalized;
};
const toAutomationTaskPanelArtifacts = (
artifacts: TaskPanelArtifact[] | undefined,
replyText: string,
runId: string
): TaskPanelArtifact[] => {
if (artifacts?.length) {
return artifacts;
}
const summary = summarizeAutomationReply(replyText);
if (!summary) {
return [];
}
return [
{
id: `automation-reply:${runId}`,
name: "
自动化执行结果
",
kind: "
回复
",
summary
}
];
};
const resolveAutomationTaskPanelExpertName = async (task: AutomationTask, projectId: string): Promise<string> => {
const taskExpertName = task.expertName?.trim();
if (taskExpertName) {
return taskExpertName;
}
const project = await projectStore.getProjectSummary(projectId).catch(() => null);
return project?.displayName?.trim()
|| project?.name?.trim()
|| (projectId === BUILTIN_HOME_PROJECT_ID ? "
通用助手
" : projectId);
};
const sendHomeImagePrompt = async (
const sendHomeImagePrompt = async (
sessionId: string,
sessionId: string,
prompt: string,
prompt: string,
...
@@ -2013,6 +2055,114 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2013,6 +2055,114 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}
}
}
}
};
};
const executeAutomationPrompt = async (sessionId: string, projectId: string, prompt: string) => {
const project = await projectStore.getProjectSummary(projectId);
const projectRoot = await projectStore.getProjectRoot(project.id);
const projectConfig = await projectStore.getProjectPackageConfig(project.id);
const snapshot = await projectContextService.getSnapshot(project.id);
const declaredWorkspaceEntryDecision = await projectExecutionRouter.decide({
sessionId,
projectId: project.id,
projectRoot,
userPrompt: prompt,
context: snapshot,
selectedSkillId: null,
attachments: [],
projectConfig
});
const preferWorkspaceEntry = declaredWorkspaceEntryDecision.kind === "
workspace
-
entry
";
const autoSkillRoute = preferWorkspaceEntry ? null : await projectSkillRouter.resolve(project.id, prompt);
const defaultEntryRoute = (!autoSkillRoute && projectConfig?.defaultEntry?.type === "
skill
")
? ((await projectStore.getProjectSkillTarget(project.id, projectConfig.defaultEntry.id))
? {
skillId: projectConfig.defaultEntry.id,
reason: `project default entry ${projectConfig.defaultEntry.id}`,
score: 0
}
: null)
: null;
const resolvedSkillRoute = autoSkillRoute ?? defaultEntryRoute;
const decision = resolvedSkillRoute
? await projectExecutionRouter.decide({
sessionId,
projectId: project.id,
projectRoot,
userPrompt: prompt,
context: snapshot,
selectedSkillId: resolvedSkillRoute.skillId,
attachments: [],
projectConfig
})
: declaredWorkspaceEntryDecision;
const executionSkillId = decision.kind === "
skill
" ? decision.skillId : undefined;
const executionPolicy = await resolveExecutionPolicy(project.id, executionSkillId, decision.kind);
const gatewayPrompt = await prepareGatewayPrompt(decision, project.id);
const shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution(decision);
runtimeCloudSupervisor.noteMessageReceived(sessionId, prompt, executionSkillId);
try {
if (decision.kind === "
workspace
-
entry
") {
const lobsterEnv = await prepareWorkspaceEntryLobsterEnv();
const projectModelEnv = await prepareProjectModelRuntime(project.id, projectRoot);
const result = await projectWorkspaceExecutor.execute({
sessionId,
projectRoot,
prompt: decision.preparedPrompt,
userPrompt: prompt,
attachments: [],
extraEnv: {
...projectModelEnv,
...lobsterEnv
}
});
if ("
handoff
" in result) {
const fallbackExecutionPolicy = await resolveExecutionPolicy(project.id, undefined, "
chat
-
fallback
");
const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "
automation
-
task
",
execute: () => gatewayClient.sendPrompt(sessionId, result.handoff.content)
});
runtimeCloudSupervisor.noteMessageSent(
fallbackResult.sessionId,
fallbackResult.reply.content,
fallbackExecutionPolicy.modelId,
executionSkillId
);
return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy, artifacts: result.artifacts };
}
runtimeCloudSupervisor.noteMessageSent(sessionId, result.reply.content, executionPolicy.modelId, executionSkillId);
return {
sessionId,
reply: result.reply,
executionPolicy,
artifacts: result.artifacts
};
}
const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "
automation
-
task
",
execute: () => gatewayClient.sendPrompt(sessionId, gatewayPrompt ?? prompt)
});
runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, executionPolicy.modelId, executionSkillId);
return { ...result, executionPolicy, artifacts: [] };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
runtimeCloudSupervisor.noteError("
automation_task_failed
", message, {
modelId: executionPolicy.modelId,
sessionId
});
throw error;
} finally {
if (shouldScheduleContextRefresh) {
void refreshProjectContextAfterExecution({
projectId: project.id,
projectContextService,
projectStore
});
}
}
};
const streamPrompt = async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[], sender?: WebContents) => {
const streamPrompt = async (sessionId: string, prompt: string, skillId?: string, attachments?: ChatAttachment[], sender?: WebContents) => {
const requestId = randomUUID();
const requestId = randomUUID();
const userMessageId = randomUUID();
const userMessageId = randomUUID();
...
@@ -2818,15 +2968,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2818,15 +2968,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
automationTaskService.setExecutor(async ({ task, run }) => {
automationTaskService.setExecutor(async ({ task, run }) => {
const projectId = task.expertId?.trim() || BUILTIN_HOME_PROJECT_ID;
const projectId = task.expertId?.trim() || BUILTIN_HOME_PROJECT_ID;
const projects = await projectStore.listProjects().catch(() => []);
const sessionId = `automation:${projectId}:${task.id}:${run.id}`;
const project = projects.find((candidate) => candidate.id === projectId);
runtimeCloudSupervisor.noteSessions([sessionId]);
const expertName = task.expertName?.trim()
const result = await executeAutomationPrompt(sessionId, projectId, task.prompt);
|| project?.displayName?.trim()
const expertName = await resolveAutomationTaskPanelExpertName(task, projectId);
|| project?.name?.trim()
|| (projectId === BUILTIN_HOME_PROJECT_ID ? "
千匠问天
" : projectId);
const session = await projectStore.createSession(`[自动化] ${task.title}`, projectId);
runtimeCloudSupervisor.noteSessions([session.id]);
const result = await sendPrompt(session.id, task.prompt);
await taskPanelService.recordWorkspaceExecution({
await taskPanelService.recordWorkspaceExecution({
sessionId: result.sessionId,
sessionId: result.sessionId,
...
@@ -2835,7 +2980,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2835,7 +2980,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
taskTitle: task.title,
taskTitle: task.title,
completedAt: new Date().toISOString(),
completedAt: new Date().toISOString(),
messageCount: 2,
messageCount: 2,
artifacts:
[]
artifacts:
toAutomationTaskPanelArtifacts(result.artifacts, result.reply.content, run.id)
}).catch((error) => startupLogger.warn("
diagnostics
", "
automation
-
task
.
archive
-
failed
", "
Failed
to
archive
automation
task
output
.
", {
}).catch((error) => startupLogger.warn("
diagnostics
", "
automation
-
task
.
archive
-
failed
", "
Failed
to
archive
automation
task
output
.
", {
taskId: task.id,
taskId: task.id,
runId: run.id,
runId: run.id,
...
@@ -2846,7 +2991,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
...
@@ -2846,7 +2991,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
sessionId: result.sessionId,
sessionId: result.sessionId,
runId: result.reply.id,
runId: result.reply.id,
replyText: result.reply.content,
replyText: result.reply.content,
artifacts:
[]
artifacts:
result.artifacts
};
};
});
});
...
...
apps/desktop/src/main/services/project-context-lifecycle.ts
View file @
c8f6b1df
...
@@ -3,7 +3,7 @@ import type { ProjectContextService } from "./project-context.js";
...
@@ -3,7 +3,7 @@ import type { ProjectContextService } from "./project-context.js";
import
type
{
ProjectStoreService
}
from
"./project-store.js"
;
import
type
{
ProjectStoreService
}
from
"./project-store.js"
;
interface
ProjectContextRefreshOptions
{
interface
ProjectContextRefreshOptions
{
sessionId
:
string
;
sessionId
?
:
string
;
projectId
:
string
;
projectId
:
string
;
projectContextService
:
ProjectContextService
;
projectContextService
:
ProjectContextService
;
projectStore
:
ProjectStoreService
;
projectStore
:
ProjectStoreService
;
...
@@ -31,6 +31,9 @@ export async function refreshProjectContextAfterExecution({
...
@@ -31,6 +31,9 @@ export async function refreshProjectContextAfterExecution({
projectContextService
.
invalidateSnapshot
(
projectId
);
projectContextService
.
invalidateSnapshot
(
projectId
);
try
{
try
{
const
snapshot
=
await
projectContextService
.
refreshSnapshot
(
projectId
);
const
snapshot
=
await
projectContextService
.
refreshSnapshot
(
projectId
);
if
(
!
sessionId
)
{
return
;
}
const
latestSessionState
=
await
projectStore
.
getSessionState
(
sessionId
);
const
latestSessionState
=
await
projectStore
.
getSessionState
(
sessionId
);
if
(
latestSessionState
.
contextSnapshotId
!==
snapshot
.
snapshotId
)
{
if
(
latestSessionState
.
contextSnapshotId
!==
snapshot
.
snapshotId
)
{
await
projectStore
.
bindSessionContextSnapshot
(
sessionId
,
snapshot
.
snapshotId
);
await
projectStore
.
bindSessionContextSnapshot
(
sessionId
,
snapshot
.
snapshotId
);
...
...
apps/desktop/test/automationTaskIpcSource.test.ts
View file @
c8f6b1df
...
@@ -7,6 +7,18 @@ const ipcSource = readFileSync(new URL("../src/main/ipc.ts", import.meta.url), "
...
@@ -7,6 +7,18 @@ const ipcSource = readFileSync(new URL("../src/main/ipc.ts", import.meta.url), "
const
indexSource
=
readFileSync
(
new
URL
(
"../src/main/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
indexSource
=
readFileSync
(
new
URL
(
"../src/main/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
preloadSource
=
readFileSync
(
new
URL
(
"../src/preload/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
const
preloadSource
=
readFileSync
(
new
URL
(
"../src/preload/index.ts"
,
import
.
meta
.
url
),
"utf8"
)
function
getAutomationExecutorBody
():
string
{
const
executorMatch
=
ipcSource
.
match
(
/automationTaskService
\.
setExecutor
\(
async
\(\{
task, run
\}\)
=>
\{(?<
body>
[\s\S]
*
?)\n
\}\)
;/
)
assert
.
ok
(
executorMatch
?.
groups
?.
body
)
return
executorMatch
.
groups
.
body
}
function
getExecuteAutomationPromptBody
():
string
{
const
bodyMatch
=
ipcSource
.
match
(
/const executeAutomationPrompt = async
\(
sessionId: string, projectId: string, prompt: string
\)
=>
\{(?<
body>
[\s\S]
*
?)\n
\}
;
\n\n
const streamPrompt = async/
)
assert
.
ok
(
bodyMatch
?.
groups
?.
body
)
return
bodyMatch
.
groups
.
body
}
test
(
"shared automation task channels and desktop API are declared"
,
()
=>
{
test
(
"shared automation task channels and desktop API are declared"
,
()
=>
{
assert
.
match
(
sharedTypesSource
,
/automationTasksList:
\s
*"automation-tasks:list"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksList:
\s
*"automation-tasks:list"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksCreate:
\s
*"automation-tasks:create"/
)
assert
.
match
(
sharedTypesSource
,
/automationTasksCreate:
\s
*"automation-tasks:create"/
)
...
@@ -26,3 +38,53 @@ test("desktop IPC wires automation tasks to AutomationTaskService", () => {
...
@@ -26,3 +38,53 @@ test("desktop IPC wires automation tasks to AutomationTaskService", () => {
assert
.
match
(
preloadSource
,
/automationTasks:
\s
*
\{
/
)
assert
.
match
(
preloadSource
,
/automationTasks:
\s
*
\{
/
)
assert
.
match
(
preloadSource
,
/IPC_CHANNELS
\.
automationTasksCreate/
)
assert
.
match
(
preloadSource
,
/IPC_CHANNELS
\.
automationTasksCreate/
)
})
})
test
(
"automation executor runs in the background without creating visible chat sessions"
,
()
=>
{
const
executorBody
=
getAutomationExecutorBody
()
assert
.
match
(
executorBody
,
/const sessionId = `automation:
\$\{
projectId
\}
:
\$\{
task
\.
id
\}
:
\$\{
run
\.
id
\}
`/
)
assert
.
doesNotMatch
(
executorBody
,
/projectStore
\.
createSession/
)
assert
.
doesNotMatch
(
executorBody
,
/sendPrompt
\(
/
)
assert
.
doesNotMatch
(
executorBody
,
/
\[
自动化
\]
/
)
})
test
(
"automation executor archives completed runs to the task panel"
,
()
=>
{
const
executorBody
=
getAutomationExecutorBody
()
assert
.
match
(
executorBody
,
/const expertName = await resolveAutomationTaskPanelExpertName
\(
task, projectId
\)
/
)
assert
.
match
(
executorBody
,
/taskPanelService
\.
recordWorkspaceExecution
\(\{
/
)
assert
.
match
(
executorBody
,
/sessionId: result
\.
sessionId/
)
assert
.
match
(
executorBody
,
/runId: run
\.
id/
)
assert
.
match
(
executorBody
,
/expertName/
)
assert
.
match
(
executorBody
,
/taskTitle: task
\.
title/
)
assert
.
match
(
executorBody
,
/messageCount: 2/
)
assert
.
match
(
executorBody
,
/artifacts: toAutomationTaskPanelArtifacts
\(
result
\.
artifacts, result
\.
reply
\.
content, run
\.
id
\)
/
)
})
test
(
"automation task panel archive uses general assistant fallback and reply artifacts"
,
()
=>
{
const
artifactHelperMatch
=
ipcSource
.
match
(
/const toAutomationTaskPanelArtifacts =
\([\s\S]
*
?\n
\}
;/
)
assert
.
ok
(
artifactHelperMatch
?.[
0
])
const
artifactHelper
=
artifactHelperMatch
[
0
]
assert
.
match
(
ipcSource
,
/task
\.
expertName
\?\.
trim
\(\)
/
)
assert
.
match
(
ipcSource
,
/project
\?\.
displayName
\?\.
trim
\(\)
/
)
assert
.
match
(
ipcSource
,
/project
\?\.
name
\?\.
trim
\(\)
/
)
assert
.
match
(
ipcSource
,
/projectId === BUILTIN_HOME_PROJECT_ID
\?
"通用助手" : projectId/
)
assert
.
match
(
artifactHelper
,
/name: "自动化执行结果"/
)
assert
.
match
(
artifactHelper
,
/kind: "回复"/
)
assert
.
doesNotMatch
(
artifactHelper
,
/path:/
)
assert
.
doesNotMatch
(
artifactHelper
,
/url:/
)
})
test
(
"automation prompt execution refreshes project context after completion without binding a visible session"
,
()
=>
{
const
automationPromptBody
=
getExecuteAutomationPromptBody
()
const
refreshCallMatch
=
automationPromptBody
.
match
(
/refreshProjectContextAfterExecution
\(\{(?<
args>
[\s\S]
*
?)\}\)
/
)
assert
.
ok
(
refreshCallMatch
?.
groups
?.
args
)
assert
.
match
(
automationPromptBody
,
/const shouldScheduleContextRefresh = shouldRefreshProjectContextAfterExecution
\(
decision
\)
/
)
assert
.
match
(
automationPromptBody
,
/finally
\s
*
\{
/
)
assert
.
match
(
refreshCallMatch
.
groups
.
args
,
/projectId: project
\.
id/
)
assert
.
match
(
refreshCallMatch
.
groups
.
args
,
/projectContextService/
)
assert
.
match
(
refreshCallMatch
.
groups
.
args
,
/projectStore/
)
assert
.
doesNotMatch
(
refreshCallMatch
.
groups
.
args
,
/sessionId/
)
})
apps/desktop/test/automationTaskService.test.ts
View file @
c8f6b1df
...
@@ -145,6 +145,12 @@ test("marks missed app-runtime schedules without auto backfill", async () => {
...
@@ -145,6 +145,12 @@ test("marks missed app-runtime schedules without auto backfill", async () => {
test
(
"manual runNow executes the configured executor and records output"
,
async
()
=>
{
test
(
"manual runNow executes the configured executor and records output"
,
async
()
=>
{
await
withService
(
async
(
service
)
=>
{
await
withService
(
async
(
service
)
=>
{
service
.
setExecutor
(
async
({
task
,
run
:
taskRun
})
=>
({
sessionId
:
`automation:home-chat:
${
task
.
id
}
:
${
taskRun
.
id
}
`
,
runId
:
taskRun
.
id
,
replyText
:
`done:
${
task
.
prompt
}
`
,
artifacts
:
[]
}))
const
task
=
await
service
.
create
({
const
task
=
await
service
.
create
({
title
:
"线索整理"
,
title
:
"线索整理"
,
prompt
:
"整理线索"
,
prompt
:
"整理线索"
,
...
@@ -155,7 +161,7 @@ test("manual runNow executes the configured executor and records output", async
...
@@ -155,7 +161,7 @@ test("manual runNow executes the configured executor and records output", async
const
run
=
await
service
.
runNow
(
task
.
id
)
const
run
=
await
service
.
runNow
(
task
.
id
)
assert
.
equal
(
run
.
status
,
"completed"
)
assert
.
equal
(
run
.
status
,
"completed"
)
assert
.
equal
(
run
.
replyText
,
"done:整理线索"
)
assert
.
equal
(
run
.
replyText
,
"done:整理线索"
)
assert
.
equal
(
run
.
sessionId
,
`
session-
${
task
.
id
}
`
)
assert
.
equal
(
run
.
sessionId
,
`
automation:home-chat:
${
task
.
id
}
:
${
run
.
id
}
`
)
const
runs
=
await
service
.
listRuns
(
task
.
id
)
const
runs
=
await
service
.
listRuns
(
task
.
id
)
assert
.
equal
(
runs
[
0
]?.
id
,
run
.
id
)
assert
.
equal
(
runs
[
0
]?.
id
,
run
.
id
)
...
...
apps/desktop/test/projectContextLifecycle.test.ts
0 → 100644
View file @
c8f6b1df
import
test
from
"node:test"
import
assert
from
"node:assert/strict"
import
{
refreshProjectContextAfterExecution
}
from
"../src/main/services/project-context-lifecycle.ts"
function
createHarness
(
options
:
{
snapshotId
?:
string
existingSnapshotId
?:
string
|
null
refreshError
?:
Error
}
=
{})
{
const
calls
:
string
[]
=
[]
const
snapshotId
=
options
.
snapshotId
??
"snapshot-new"
const
existingSnapshotId
=
options
.
existingSnapshotId
??
null
const
refreshError
=
options
.
refreshError
const
projectContextService
=
{
invalidateSnapshot
(
projectId
:
string
)
{
calls
.
push
(
`invalidate:
${
projectId
}
`
)
},
async
refreshSnapshot
(
projectId
:
string
)
{
calls
.
push
(
`refresh:
${
projectId
}
`
)
if
(
refreshError
)
{
throw
refreshError
}
return
{
snapshotId
}
}
}
const
projectStore
=
{
async
getSessionState
(
sessionId
:
string
)
{
calls
.
push
(
`get-session:
${
sessionId
}
`
)
return
{
contextSnapshotId
:
existingSnapshotId
}
},
async
bindSessionContextSnapshot
(
sessionId
:
string
,
nextSnapshotId
:
string
)
{
calls
.
push
(
`bind-session:
${
sessionId
}
:
${
nextSnapshotId
}
`
)
}
}
return
{
calls
,
projectContextService
,
projectStore
}
}
test
(
"refreshes project context without reading session state when sessionId is omitted"
,
async
()
=>
{
const
{
calls
,
projectContextService
,
projectStore
}
=
createHarness
()
await
refreshProjectContextAfterExecution
({
projectId
:
"project-1"
,
projectContextService
,
projectStore
})
assert
.
deepEqual
(
calls
,
[
"invalidate:project-1"
,
"refresh:project-1"
])
})
test
(
"binds the refreshed snapshot to a provided session"
,
async
()
=>
{
const
{
calls
,
projectContextService
,
projectStore
}
=
createHarness
({
snapshotId
:
"snapshot-new"
,
existingSnapshotId
:
"snapshot-old"
})
await
refreshProjectContextAfterExecution
({
sessionId
:
"project:project-1"
,
projectId
:
"project-1"
,
projectContextService
,
projectStore
})
assert
.
deepEqual
(
calls
,
[
"invalidate:project-1"
,
"refresh:project-1"
,
"get-session:project:project-1"
,
"bind-session:project:project-1:snapshot-new"
])
})
test
(
"invalidates again and does not throw when refresh fails"
,
async
()
=>
{
const
{
calls
,
projectContextService
,
projectStore
}
=
createHarness
({
refreshError
:
new
Error
(
"refresh failed"
)
})
await
refreshProjectContextAfterExecution
({
projectId
:
"project-1"
,
projectContextService
,
projectStore
})
assert
.
deepEqual
(
calls
,
[
"invalidate:project-1"
,
"refresh:project-1"
,
"invalidate:project-1"
])
})
apps/desktop/test/taskPanelService.test.ts
View file @
c8f6b1df
...
@@ -79,6 +79,31 @@ test("dedupes repeated runs and repeated artifact paths", async () => {
...
@@ -79,6 +79,31 @@ test("dedupes repeated runs and repeated artifact paths", async () => {
})
})
})
})
test
(
"records pathless automation reply artifacts as visible outputs"
,
async
()
=>
{
await
withService
(
async
(
service
)
=>
{
await
service
.
recordWorkspaceExecution
({
sessionId
:
"automation:home-chat:task-1:run-1"
,
runId
:
"run-1"
,
date
:
"2026-05-15"
,
expertName
:
"通用助手"
,
taskTitle
:
"自动化回复"
,
completedAt
:
"2026-05-15T02:00:00.000Z"
,
artifacts
:
[
{
id
:
"reply-1"
,
name
:
"自动化执行结果"
,
kind
:
"回复"
,
summary
:
"已整理完成"
}
]
})
const
items
=
await
service
.
listByDate
(
"2026-05-15"
)
assert
.
equal
(
items
.
length
,
1
)
assert
.
equal
(
items
[
0
]?.
artifacts
.
length
,
1
)
assert
.
equal
(
items
[
0
]?.
artifacts
[
0
]?.
name
,
"自动化执行结果"
)
assert
.
equal
(
items
[
0
]?.
artifacts
[
0
]?.
kind
,
"回复"
)
assert
.
equal
(
items
[
0
]?.
artifacts
[
0
]?.
summary
,
"已整理完成"
)
assert
.
equal
(
items
[
0
]?.
artifacts
[
0
]?.
path
,
undefined
)
assert
.
equal
(
items
[
0
]?.
artifacts
[
0
]?.
url
,
undefined
)
})
})
test
(
"preserves all tasks when multiple executions are recorded concurrently"
,
async
()
=>
{
test
(
"preserves all tasks when multiple executions are recorded concurrently"
,
async
()
=>
{
await
withService
(
async
(
service
)
=>
{
await
withService
(
async
(
service
)
=>
{
await
Promise
.
all
(
Array
.
from
({
length
:
12
},
(
_
,
index
)
=>
service
.
recordWorkspaceExecution
({
await
Promise
.
all
(
Array
.
from
({
length
:
12
},
(
_
,
index
)
=>
service
.
recordWorkspaceExecution
({
...
...
apps/ui/test/taskPanelViewSource.test.ts
View file @
c8f6b1df
...
@@ -18,6 +18,13 @@ test("task panel output uses real artifact paths and produced timestamps", () =>
...
@@ -18,6 +18,13 @@ test("task panel output uses real artifact paths and produced timestamps", () =>
assert
.
match
(
source
,
/artifact
\.
producedAt
\s
*
\?\?
/
)
assert
.
match
(
source
,
/artifact
\.
producedAt
\s
*
\?\?
/
)
})
})
test
(
"task panel output renders pathless artifacts without copy controls"
,
()
=>
{
assert
.
match
(
source
,
/const artifactPath = artifact
\.
path
\?\?
artifact
\.
url/
)
assert
.
match
(
source
,
/<strong title=
\{
artifact
\.
name
\}
>
\{
artifact
\.
name
\}
<
\/
strong>/
)
assert
.
match
(
source
,
/<p>
\{
artifact
\.
summary
\?\?
task
\.
taskTitle
\}
<
\/
p>/
)
assert
.
match
(
source
,
/
\{
artifactPath
\?
\(
/
)
})
test
(
"task panel output copy feedback is scoped to the clicked output row"
,
()
=>
{
test
(
"task panel output copy feedback is scoped to the clicked output row"
,
()
=>
{
assert
.
match
(
source
,
/copiedOutputRowKey/
)
assert
.
match
(
source
,
/copiedOutputRowKey/
)
assert
.
match
(
source
,
/const outputRowKey = getTaskPanelOutputRowKey
\(
task, artifact
\)
/
)
assert
.
match
(
source
,
/const outputRowKey = getTaskPanelOutputRowKey
\(
task, artifact
\)
/
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment