diff --git a/docs/verification/issue-370-completed-context-navigation.md b/docs/verification/issue-370-completed-context-navigation.md
index f1fb3e7..393470c 100644
--- a/docs/verification/issue-370-completed-context-navigation.md
+++ b/docs/verification/issue-370-completed-context-navigation.md
@@ -35,4 +35,6 @@
6. 开发环境页面发现按钮未继承操作样式、输入区提示与主内容重复。历史入口复用现有 `taskActions` 与 `row` 样式;历史上下文不再单独生成输入区提示,原有正式交接提示保持。3 个相关 UI 测试文件共 58 项通过,类型检查通过。原始记录为本地 `/tmp/370-visual-tests.log`、`/tmp/370-visual-typecheck.log` 与 `/tmp/370-visual-client-typecheck.log`;测试 mock 的字面量类型修正后,客户端类型检查已单独通过,宿主检查复用同轮通过结果。
+7. 实际验收发现历史导航等待旧执行的 `task.detail` 完整历史观察,导致入口显示或点击受阻。仅调整客户端:历史入口使用已有 `session.work.historicalContexts` 投影显示;点击通过现有 `project.detail` 找到准确项目与 Task,核验完成事实及目标关联。普通当前会话导航仍使用 `task.detail`,后端查询与执行图行为不变。生产组件在 `task.detail` 永久不返回时仍显示并准确导航;新读数缺少任务、完成记录或精确关联时拒绝。2 个相关测试文件共 61 项通过,客户端类型检查通过。原始记录为本地 `/tmp/370-history-read-red.log`、`/tmp/370-history-read-tests.log` 与 `/tmp/370-history-read-typecheck.log`。
+
本报告只记录代码级验证。实际开发环境页面截图、发布、既有生产后续会话登记与最终验收由主 agent 继续执行;本分支未部署、未重启服务、未修改生产数据。
diff --git a/src/client/execution-native.tsx b/src/client/execution-native.tsx
index b0c6297..6341993 100644
--- a/src/client/execution-native.tsx
+++ b/src/client/execution-native.tsx
@@ -115,7 +115,7 @@ function NativeTaskContinuation({ controller, sessionId, work }: { controller: W
{error &&
{error}
}
- {completed &&
binding.historicalSource?.sessionId === sessionId && binding.historicalSource.runtimeId === detail.execution?.resource.runtimeId).map(binding => binding.session)} busy={busy} onEnter={session => void enterContext(session)}/>}
+ void enterContext(session)}/>
{detail ? void controlWork('continue'), onRecover: () => void controlWork('reconcile'), onResume: () => void controlWork('resume') } : {})}/> : !error && 正在读取接续事实…
}
diff --git a/src/client/work-panel-controller.ts b/src/client/work-panel-controller.ts
index 0510144..780e253 100644
--- a/src/client/work-panel-controller.ts
+++ b/src/client/work-panel-controller.ts
@@ -1022,12 +1022,11 @@ export class WorkPanelController {
signal?.addEventListener('abort', cancel, { once: true })
const revision = this.revision, sourceRevision = this.sourceRevision
try {
- const detail = await this.client.query({ type: 'task.detail', ref }, abort.signal)
+ const detail = await this.client.query(historicalContext ? { type: 'project.detail', projectId: ref.projectId } : { type: 'task.detail', ref }, abort.signal)
if (abort.signal.aborted || revision !== this.revision || sourceRevision !== this.sourceRevision) return
if (historicalContext) {
- const task = detail.task
- if (task?.id !== ref.taskId || task.projectId !== ref.projectId || !task.completion
- || !task.sessions.some(binding => binding.purpose === 'related' && binding.historicalSource
+ const task = detail.tasks?.find(item => item.id === ref.taskId && item.projectId === ref.projectId)
+ if (!task?.completion || !task.sessions.some(binding => binding.purpose === 'related' && binding.historicalSource
&& binding.session.runtimeId === historicalContext.runtimeId && binding.session.sessionId === historicalContext.sessionId)) throw Error('后续上下文暂不可进入,请刷新工作详情。')
this.openSession(historicalContext)
return
diff --git a/tests/client/execution-continuation-render.spec.ts b/tests/client/execution-continuation-render.spec.ts
index 3f78ad6..b80aee7 100644
--- a/tests/client/execution-continuation-render.spec.ts
+++ b/tests/client/execution-continuation-render.spec.ts
@@ -17,6 +17,7 @@ function controllerFor(detail: WorkCompleted, purpose: AssociatedWork['purpose']
command: vi.fn(async () => { throw Error('Render must not write') }),
query: async query => {
if (query.type === 'task.detail') return detail
+ if (query.type === 'project.detail') return { status: 'completed', tasks: detail.task ? [detail.task] : [] }
if (query.type === 'resources.list') return { status: 'completed', resources: [{ ref: { runtimeId: 'runtime', workspaceId: 'ws' }, title: 'Workspace' }] }
if (query.type === 'session.work') return { status: 'completed', work: [{ ref, title: 'Task continuation', projectName: 'Project', purpose }] }
return { status: 'completed' }
@@ -222,11 +223,20 @@ it.each(['native', 'header', 'task'] as const)('opens the exact saved historical
expect(detail.task!.completion).toEqual(completion)
})
-it('rejects a stale historical context link rather than substituting the canonical executor', async () => {
- const detail = taskDetail(), controller = controllerFor(detail)
+it.each(['missingTask', 'wrongProject', 'unfinished', 'removedBinding', 'reviewBinding', 'missingSource', 'wrongRuntime'] as const)('rejects %s historical context from the fresh project read without entering the canonical executor', async invalid => {
+ const detail = taskDetail(), successor = { runtimeId: 'runtime', sessionId: 'session-existing-context' }
detail.task!.completion = completion
- const entered = vi.spyOn(controller, 'openSession')
- await expect(controller.openHistoricalContext(ref, { runtimeId: 'runtime', sessionId: 'unregistered' })).rejects.toThrow('后续上下文暂不可进入')
+ detail.task!.sessions = [{ session: successor, purpose: 'related', historicalSource: session }]
+ if (invalid === 'missingTask') delete detail.task
+ if (invalid === 'wrongProject') detail.task!.projectId = 'different-project'
+ if (invalid === 'unfinished') delete detail.task!.completion
+ if (invalid === 'removedBinding') detail.task!.sessions = []
+ if (invalid === 'reviewBinding') detail.task!.sessions[0]!.purpose = 'review'
+ if (invalid === 'missingSource') delete detail.task!.sessions[0]!.historicalSource
+ if (invalid === 'wrongRuntime') detail.task!.sessions[0]!.session = { ...successor, runtimeId: 'foreign-runtime' }
+ const controller = controllerFor(detail), query = vi.spyOn(controller.client, 'query'), entered = vi.spyOn(controller, 'openSession')
+ await expect(controller.openHistoricalContext(ref, successor)).rejects.toThrow('后续上下文暂不可进入')
+ expect(query).toHaveBeenCalledExactlyOnceWith({ type: 'project.detail', projectId: ref.projectId }, expect.any(AbortSignal))
expect(entered).not.toHaveBeenCalled()
expect(controller.client.command).not.toHaveBeenCalled()
})
@@ -254,3 +264,28 @@ it.each(['control','history'] as const)('keeps the %s dock free of historical-co
}
expect(controller.client.command).not.toHaveBeenCalled()
})
+
+
+it('shows and opens projected historical context while the old execution detail never resolves', async () => {
+ const detail = taskDetail(), successor = { runtimeId: 'runtime', sessionId: 'session-existing-context' }
+ detail.task!.completion = completion
+ detail.task!.sessions = [{ session: successor, purpose: 'related', historicalSource: session }]
+ const controller = controllerFor(detail), query = controller.client.query.bind(controller.client)
+ controller.client.query = vi.fn(async (value, signal): Promise => {
+ if (value.type === 'task.detail') return new Promise(() => {})
+ if (value.type === 'session.work') return { status: 'completed', work: [{ ref, title: detail.task!.title, projectName: 'Project', purpose: 'control', historicalContexts: [successor] }] }
+ return query(value, signal)
+ })
+ const entered = vi.spyOn(controller, 'openSession')
+ const view = mountProductionComponent({ controller, sessionId: 'current' }, 'ExecutionConversationContinuation', new URL('../../src/client/execution-native.tsx', import.meta.url))
+ mounted.push(view)
+ await expect.poll(() => view.text()).toContain('后续上下文')
+ expect(view.text()).toContain('正在读取接续事实')
+ expect(entered).not.toHaveBeenCalled()
+ const before = vi.mocked(controller.client.query).mock.calls.length
+ view.click('进入会话')
+ await expect.poll(() => entered.mock.calls.length).toBe(1)
+ expect(entered).toHaveBeenCalledExactlyOnceWith(successor)
+ expect(vi.mocked(controller.client.query).mock.calls.slice(before).map(([value]) => value)).toEqual([{ type: 'project.detail', projectId: ref.projectId }])
+ expect(controller.client.command).not.toHaveBeenCalled()
+})