mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
ea58bd7526
* fix: preserve ChatPanel history and clarification state * test(daemon): align artifact replay assertions with durable history * fix(web): stop replaying OD Next turns parked on the user A clarification turn ends with its Run succeeded while the strategy task stays open (`clarification_required`, `plan_ready`). The recovery probe in `attachRecoverableRuns` only bailed out for terminal tasks, so it cleared the finished row and replayed the Run from event 0. A conversation refresh landing mid-replay adopted the persisted transcript, the rest of the replay was appended behind it, and the replay's terminal PUT stored the duplicate: the question form card rendered with raw form JSON under it. A succeeded Run whose task holds no active Run has nothing left to follow; the daemon claims any automatic successor in the same transaction as the verdict, which a probe would see as a different `activeRunId`. Seal such a Run without clearing or replaying it. Rows that still need transcript recovery keep replaying. Refs OPEND-3230
182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { ChatRunStatusResponse, StrategyTaskProjectionV2 } from '@open-design/contracts';
|
|
import {
|
|
resolveQuestionFormStrategyTaskExecutionId,
|
|
strategyBlockedMessageFields,
|
|
strategySettledMessageFields,
|
|
strategyTaskParkedOnSucceededRun,
|
|
} from '../../src/runtime/strategy-question-continuation';
|
|
|
|
describe('question-form strategy continuation handle recovery', () => {
|
|
it('recovers the task handle from status during the same-render projection race', async () => {
|
|
const fetchRunStatus = vi.fn(async () => ({
|
|
strategyTask: { taskExecutionId: 'task-1' },
|
|
} as ChatRunStatusResponse));
|
|
|
|
await expect(resolveQuestionFormStrategyTaskExecutionId({
|
|
sourceRunId: 'run-1',
|
|
fetchRunStatus,
|
|
})).resolves.toBe('task-1');
|
|
expect(fetchRunStatus).toHaveBeenCalledWith('run-1');
|
|
});
|
|
|
|
it('keeps an ordinary question form ordinary when status has no task', async () => {
|
|
const fetchRunStatus = vi.fn(async () => ({ status: 'succeeded' } as ChatRunStatusResponse));
|
|
|
|
await expect(resolveQuestionFormStrategyTaskExecutionId({
|
|
sourceRunId: 'run-ordinary',
|
|
fetchRunStatus,
|
|
})).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('does not reject or block submission when status recovery fails', async () => {
|
|
const fetchRunStatus = vi.fn(async () => {
|
|
throw new Error('daemon unavailable');
|
|
});
|
|
const submit = vi.fn();
|
|
|
|
const taskExecutionId = await resolveQuestionFormStrategyTaskExecutionId({
|
|
sourceRunId: 'run-unavailable',
|
|
fetchRunStatus,
|
|
});
|
|
submit(taskExecutionId);
|
|
|
|
expect(taskExecutionId).toBeUndefined();
|
|
expect(submit).toHaveBeenCalledWith(undefined);
|
|
});
|
|
});
|
|
|
|
function blockedProjection(
|
|
overrides: Partial<StrategyTaskProjectionV2> = {},
|
|
): StrategyTaskProjectionV2 {
|
|
return {
|
|
taskExecutionId: 'task-1',
|
|
strategy: {
|
|
id: 'od-next-strategy',
|
|
version: '2.0.0',
|
|
packageHash: 'a'.repeat(64),
|
|
snapshotId: 'snapshot-1',
|
|
},
|
|
inputStage: 'request',
|
|
outcome: 'blocked',
|
|
route: 'full_plan',
|
|
executionMode: null,
|
|
activeRunId: 'run-1',
|
|
terminal: true,
|
|
...overrides,
|
|
} as StrategyTaskProjectionV2;
|
|
}
|
|
|
|
describe('strategyBlockedMessageFields', () => {
|
|
it('derives message termination fields from a blocked terminal projection', () => {
|
|
expect(strategyBlockedMessageFields(blockedProjection({
|
|
blockedContext: {
|
|
reasonCodes: ['od_next_machine_protocol_missing'],
|
|
visibleText: ' 这轮回复没有携带机器协议块。 ',
|
|
},
|
|
}))).toEqual({
|
|
strategyTaskBlocked: true,
|
|
strategyTaskBlockedText: '这轮回复没有携带机器协议块。',
|
|
});
|
|
});
|
|
|
|
it('keeps the blocked flag with a null text when the gate left no visible text', () => {
|
|
expect(strategyBlockedMessageFields(blockedProjection({
|
|
blockedContext: {
|
|
reasonCodes: ['od_next_native_session_continuity_unproven'],
|
|
visibleText: null,
|
|
},
|
|
}))).toEqual({ strategyTaskBlocked: true, strategyTaskBlockedText: null });
|
|
expect(strategyBlockedMessageFields(blockedProjection())).toEqual({
|
|
strategyTaskBlocked: true,
|
|
strategyTaskBlockedText: null,
|
|
});
|
|
});
|
|
|
|
it('returns null for non-blocked or absent projections', () => {
|
|
expect(strategyBlockedMessageFields(undefined)).toBeNull();
|
|
expect(strategyBlockedMessageFields(blockedProjection({
|
|
outcome: 'completed',
|
|
}))).toBeNull();
|
|
expect(strategyBlockedMessageFields(blockedProjection({
|
|
outcome: 'running',
|
|
terminal: false,
|
|
}))).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('strategySettledMessageFields', () => {
|
|
it('stamps a delivered flag for a completed task', () => {
|
|
expect(strategySettledMessageFields(blockedProjection({
|
|
outcome: 'completed',
|
|
terminal: true,
|
|
blockedContext: undefined,
|
|
}))).toEqual({ strategyTaskDelivered: true });
|
|
});
|
|
|
|
it('keeps the blocked stamp taking precedence', () => {
|
|
expect(strategySettledMessageFields(blockedProjection())).toMatchObject({
|
|
strategyTaskBlocked: true,
|
|
});
|
|
});
|
|
|
|
it('stamps nothing while the task is still running', () => {
|
|
expect(strategySettledMessageFields(blockedProjection({
|
|
outcome: 'running',
|
|
terminal: false,
|
|
blockedContext: undefined,
|
|
}))).toBeNull();
|
|
expect(strategySettledMessageFields(undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('strategyTaskParkedOnSucceededRun', () => {
|
|
const parked = (overrides: Partial<StrategyTaskProjectionV2> = {}) => blockedProjection({
|
|
outcome: 'clarification_required',
|
|
terminal: false,
|
|
blockedContext: undefined,
|
|
...overrides,
|
|
});
|
|
|
|
it.each(['clarification_required', 'plan_ready'] as const)(
|
|
'is true when a succeeded Run left its task waiting on the user (%s)',
|
|
(outcome) => {
|
|
expect(strategyTaskParkedOnSucceededRun(
|
|
{ status: 'succeeded', strategyTask: parked({ outcome }) },
|
|
'run-1',
|
|
)).toBe(true);
|
|
},
|
|
);
|
|
|
|
it('is false while the task still runs, even on the same Run', () => {
|
|
expect(strategyTaskParkedOnSucceededRun(
|
|
{ status: 'succeeded', strategyTask: parked({ outcome: 'running' }) },
|
|
'run-1',
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('is false when the task has moved on to another Run', () => {
|
|
expect(strategyTaskParkedOnSucceededRun(
|
|
{ status: 'succeeded', strategyTask: parked({ activeRunId: 'run-2' }) },
|
|
'run-1',
|
|
)).toBe(false);
|
|
});
|
|
|
|
it('is false while the Run itself has not succeeded', () => {
|
|
for (const status of ['queued', 'running', 'failed', 'canceled'] as const) {
|
|
expect(strategyTaskParkedOnSucceededRun(
|
|
{ status, strategyTask: parked() },
|
|
'run-1',
|
|
)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('leaves terminal tasks and task-less Runs to their own checks', () => {
|
|
expect(strategyTaskParkedOnSucceededRun(
|
|
{ status: 'succeeded', strategyTask: blockedProjection() },
|
|
'run-1',
|
|
)).toBe(false);
|
|
expect(strategyTaskParkedOnSucceededRun({ status: 'succeeded' }, 'run-1')).toBe(false);
|
|
});
|
|
});
|