@tea-agent/loop-agent 0.42.0-next.4 → 0.42.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/worker/console/chat/pi-runtime.js +26 -0
- package/dist/worker/console/chat/routes.js +60 -5
- package/dist/worker/console/chat/sdd-data-alignment.js +219 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +26 -0
- package/dist/worker/console/chat/shortcuts.js +18 -2
- package/dist/worker/console/server.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CeznJ4Iw.js → abnfDiagram-N423BO3Z-BJpsGgAv.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4u9u7SX.js → arc-DjMFPsEd.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-oghpT81p.js → architectureDiagram-T3A2C74G-DqdZlr1O.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BPJ5Kvcj.js → blockDiagram-VBNYF7ZC-BmZnfFEO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CoIu69pd.js → c4Diagram-5PPSVZJV-3cKberpL.js} +1 -1
- package/dist/worker/console/static/assets/channel-DWYwciiv.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DtgiAsSc.js → chunk-2GRJ4B5K-q-0yjQ1h.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-YSu_EVSD.js → chunk-2Q5K7J3B-DnONBaDW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DaRO-NB6.js → chunk-5RXB4S5H-v3a6PCOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Dh0ZKiGA.js → chunk-5VM5RSS4-D1kLrlU_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DOrvbR-H.js → chunk-6Q2QTUOP-BPelzihi.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3b22LN2.js → chunk-GF5L2VYU-DauNE69H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CXlqO8l6.js → chunk-JWPE2WC7-RIg5_bFJ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BblTBrDu.js → chunk-KBJHAD2P-BuILuoxs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DswqPALe.js → chunk-RYQCIY6F-VHORV5oW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CfTwVOlc.js → chunk-XXDRQBXY-BJYr4xHC.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-bLD8jVFo.js → cose-bilkent-JH36ORCC-BS6nuqQn.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvoG1fh5.js → cynefin-VYW2F7L2-BBhF0NAT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DEPAdJu3.js → cynefinDiagram-MW4NZA55-D1135Ifg.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmrNvBLy.js → dagre-VZM6K2ZE-vAOYPUWJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdwzIwvs.js → diagram-7IWD3JNH-DuaSPkfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CJ3hj-md.js → diagram-B4RE2ZJO-mMvXvGU6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C6YJT2a4.js → diagram-LBJQPF4R-CDIEJtlL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C_LPfjhX.js → diagram-Q27KOJAE-Bi_6JvCW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-xvsSb5Ie.js → diagram-UB23O5K3-DAqj57yN.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Rxp2tKcD.js → ebnfDiagram-BXEA7PRR-DfWaxNEo.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DRyLYmZh.js → erDiagram-JOGREHBK-Bt7QqkNG.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-TLSU_D2B.js → flowDiagram-UKHOOZJN-CRRhQwT2.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGqeTvHC.js → ganttDiagram-PKOTCBZU-DAV1O5B6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BZxyH2wh.js → gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js} +1 -1
- package/dist/worker/console/static/assets/index-CVdddQsA.js +451 -0
- package/dist/worker/console/static/assets/index-DDQc5a50.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BsYtzx01.js → infoDiagram-6WML65LV-C5W5eILC.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js → ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DYbB3ibo.js → journeyDiagram-NVQOT4AX-CKDRcaKf.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BHJHewZO.js → kanban-definition-27J2QSJJ-Cc7_TuSb.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CmPIPmCG.js → linear-BcQYYmeT.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-F--C1uV0.js → mermaid.core-CwLZPtQo.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-YTXfL1st.js → mindmap-definition-FAOFIHXS-D29M025z.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DdDMYXP9.js → pegDiagram-VL7TDLO6-CV-ZAWYe.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C3nH4KVd.js → pieDiagram-7S7Q4E2Y-C6iPS_o5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js → quadrantDiagram-CIZ2JOQS-BFCD1sTg.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-KNzbl-7w.js → railroadDiagram-AXF67PYL-CztNj4O9.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CSwI6dG0.js → requirementDiagram-LRYGKXZP-BWIELSNd.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bl1AyuXy.js → sankeyDiagram-W5VNT64P-CPtxrjJQ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2y4EmXQa.js → sequenceDiagram-SI44F4Z6-BEWVSFsu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-E8v8YrMg.js → sizeCapture-X5ZJPWSS-DyhzoX_J.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eGGlR856.js → stateDiagram-OKZ733FA-xLox0nu7.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-B7g7xc07.js → swimlanes-SLNWSIFB-CLC_ow9A.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DR3VCHBG.js → timeline-definition-Z64GVDOM-DmJZ4uyY.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LUvapZIy.js → vennDiagram-T6HMQDX7-DD7GGi9Z.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C7GDcaXq.js → wardleyDiagram-T6FBY63Y-Bi8PQoGt.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CbNAqoGK.js → xychartDiagram-ELKLHX3M-uKdAY8Dp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +225 -21
- package/dist/worker/console/static-src/operator-chat/useComposer.js +5 -1
- package/dist/worker/console/workspace-context.js +4 -0
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +58 -15
- package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +16 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +42 -6
- package/dist/workflows/dag/init-hybrid.js +62 -22
- package/dist/workflows/dag/prompt.js +36 -2
- package/docs/templates/backend-test-dag.json +5 -5
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +0 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +0 -451
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +0 -8
|
@@ -6,6 +6,94 @@ import { confirmationToken } from "./format.js";
|
|
|
6
6
|
import { upsertCompactionMessage } from "./compaction-message.js";
|
|
7
7
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
8
8
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
9
|
+
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
10
|
+
export function insertOptimisticSession(list, row) {
|
|
11
|
+
return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
|
|
12
|
+
}
|
|
13
|
+
export function replaceOptimisticSession(list, temporaryId, durable) {
|
|
14
|
+
return [
|
|
15
|
+
durable,
|
|
16
|
+
...list.filter((item) => item.sessionId !== durable.sessionId &&
|
|
17
|
+
(!temporaryId || item.sessionId !== temporaryId)),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
export function removeSessionById(list, sessionId) {
|
|
21
|
+
const index = list.findIndex((item) => item.sessionId === sessionId);
|
|
22
|
+
if (index < 0)
|
|
23
|
+
return { next: list, index: -1 };
|
|
24
|
+
return {
|
|
25
|
+
next: list.filter((item) => item.sessionId !== sessionId),
|
|
26
|
+
removed: list[index],
|
|
27
|
+
index,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function restoreSessionAt(list, row, index) {
|
|
31
|
+
if (list.some((item) => item.sessionId === row.sessionId))
|
|
32
|
+
return list;
|
|
33
|
+
const next = [...list];
|
|
34
|
+
next.splice(Math.max(0, Math.min(index, next.length)), 0, row);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
function sameQueueSummary(a, b) {
|
|
38
|
+
const left = a.deferredTurns ?? (a.deferredTurn ? [a.deferredTurn] : []);
|
|
39
|
+
const right = b.deferredTurns ?? (b.deferredTurn ? [b.deferredTurn] : []);
|
|
40
|
+
if (left.length !== right.length)
|
|
41
|
+
return false;
|
|
42
|
+
return left.every((item, index) => {
|
|
43
|
+
const other = right[index];
|
|
44
|
+
return Boolean(other &&
|
|
45
|
+
item.clientRequestId === other.clientRequestId &&
|
|
46
|
+
item.revision === other.revision &&
|
|
47
|
+
item.state === other.state &&
|
|
48
|
+
item.updatedAt === other.updatedAt &&
|
|
49
|
+
item.textPreview === other.textPreview &&
|
|
50
|
+
item.imageCount === other.imageCount &&
|
|
51
|
+
item.acceptedTurnId === other.acceptedTurnId &&
|
|
52
|
+
item.lastError?.code === other.lastError?.code &&
|
|
53
|
+
item.lastError?.message === other.lastError?.message);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function sameSessionSummary(a, b) {
|
|
57
|
+
return (a.sessionId === b.sessionId &&
|
|
58
|
+
a.workspaceId === b.workspaceId &&
|
|
59
|
+
a.state === b.state &&
|
|
60
|
+
a.title === b.title &&
|
|
61
|
+
a.displayTitle === b.displayTitle &&
|
|
62
|
+
a.updatedAt === b.updatedAt &&
|
|
63
|
+
a.activeTurnId === b.activeTurnId &&
|
|
64
|
+
a.turnPhase === b.turnPhase &&
|
|
65
|
+
a.model?.provider === b.model?.provider &&
|
|
66
|
+
a.model?.modelId === b.model?.modelId &&
|
|
67
|
+
a.thinkingLevel === b.thinkingLevel &&
|
|
68
|
+
a.isBlank === b.isBlank &&
|
|
69
|
+
a.optimisticState === b.optimisticState &&
|
|
70
|
+
sameQueueSummary(a, b));
|
|
71
|
+
}
|
|
72
|
+
/** Reuse unchanged row and list references across catalog refreshes. */
|
|
73
|
+
export function stabilizeSessionList(previous, next) {
|
|
74
|
+
const previousById = new Map(previous.map((item) => [item.sessionId, item]));
|
|
75
|
+
let changed = previous.length !== next.length;
|
|
76
|
+
const stabilized = next.map((item, index) => {
|
|
77
|
+
const existing = previousById.get(item.sessionId);
|
|
78
|
+
const value = existing && sameSessionSummary(existing, item) ? existing : item;
|
|
79
|
+
if (value !== previous[index])
|
|
80
|
+
changed = true;
|
|
81
|
+
return value;
|
|
82
|
+
});
|
|
83
|
+
return changed ? stabilized : previous;
|
|
84
|
+
}
|
|
85
|
+
/** Select a server-confirmed blank session owned by the requested workspace. */
|
|
86
|
+
export function findReusableBlankSession(list, workspaceId) {
|
|
87
|
+
const scope = workspaceId.trim();
|
|
88
|
+
if (!scope)
|
|
89
|
+
return undefined;
|
|
90
|
+
return list.find((item) => item.workspaceId === scope &&
|
|
91
|
+
item.state === "active" &&
|
|
92
|
+
item.isBlank === true &&
|
|
93
|
+
item.turnPhase === "idle" &&
|
|
94
|
+
!item.activeTurnId &&
|
|
95
|
+
!item.optimisticState);
|
|
96
|
+
}
|
|
9
97
|
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
10
98
|
if (input.creatingRef?.current) {
|
|
11
99
|
throw new Error("CREATE_SESSION_IN_FLIGHT");
|
|
@@ -23,7 +111,10 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
23
111
|
"x-loop-console-confirmation": confirmationToken(),
|
|
24
112
|
},
|
|
25
113
|
credentials: "include",
|
|
26
|
-
body: JSON.stringify(
|
|
114
|
+
body: JSON.stringify((() => {
|
|
115
|
+
const { optimisticSessionId: _optimisticSessionId, ...body } = input.options ?? {};
|
|
116
|
+
return body;
|
|
117
|
+
})()),
|
|
27
118
|
});
|
|
28
119
|
if (!res.ok) {
|
|
29
120
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -35,7 +126,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
35
126
|
const body = (await res.json());
|
|
36
127
|
if (!body.session)
|
|
37
128
|
throw new Error("创建会话响应缺少 durable session");
|
|
38
|
-
const durable = body.session;
|
|
129
|
+
const durable = { ...body.session, isBlank: true };
|
|
39
130
|
const created = {
|
|
40
131
|
sessionId: durable.sessionId,
|
|
41
132
|
...(durable.workspaceId || input.options?.workspaceId
|
|
@@ -45,16 +136,12 @@ export async function createSessionAndPrimeTitleWatcher(input) {
|
|
|
45
136
|
: {}),
|
|
46
137
|
...runtimeSelectionFromRecord(durable),
|
|
47
138
|
};
|
|
48
|
-
const nextList =
|
|
49
|
-
durable,
|
|
50
|
-
...input.sessionListRef.current.filter((item) => item.sessionId !== durable.sessionId),
|
|
51
|
-
];
|
|
139
|
+
const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
|
|
52
140
|
// Commit ref → state → eligibility → deferred refresh. The ref assignment
|
|
53
141
|
// makes the durable record observable before React's state commit.
|
|
54
142
|
input.sessionListRef.current = nextList;
|
|
55
143
|
input.setSessionList(nextList);
|
|
56
144
|
input.titleWatcher?.markEligible(durable.sessionId);
|
|
57
|
-
void input.refreshSessionList();
|
|
58
145
|
return created;
|
|
59
146
|
}
|
|
60
147
|
finally {
|
|
@@ -90,6 +177,11 @@ export function useChatSessions(params) {
|
|
|
90
177
|
}, [refs]);
|
|
91
178
|
const [sessionList, setSessionList] = useState([]);
|
|
92
179
|
const sessionListRef = useRef([]);
|
|
180
|
+
const pendingDeletedIdsRef = useRef(new Set());
|
|
181
|
+
const pendingCreatedRowsRef = useRef(new Map());
|
|
182
|
+
const sessionMutationVersionRef = useRef(0);
|
|
183
|
+
const deleteRequestVersionsRef = useRef(new Map());
|
|
184
|
+
const activateSessionRef = useRef(null);
|
|
93
185
|
sessionListRef.current = sessionList;
|
|
94
186
|
const titleWatcherRef = useRef(null);
|
|
95
187
|
// Shared busy gate for sidebar + Landing create entries (one render feedback).
|
|
@@ -159,9 +251,13 @@ export function useChatSessions(params) {
|
|
|
159
251
|
return;
|
|
160
252
|
}
|
|
161
253
|
const sessions = body.sessions ?? [];
|
|
254
|
+
const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
|
|
255
|
+
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
|
|
162
256
|
// Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
|
|
163
257
|
// visible recent-item limit independently inside each workspace group.
|
|
164
|
-
|
|
258
|
+
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
259
|
+
sessionListRef.current = next;
|
|
260
|
+
setSessionList(next);
|
|
165
261
|
if (body.workspaceErrors?.length) {
|
|
166
262
|
setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
|
|
167
263
|
}
|
|
@@ -171,6 +267,60 @@ export function useChatSessions(params) {
|
|
|
171
267
|
}
|
|
172
268
|
}, [origin, blocked, setError]);
|
|
173
269
|
const deleteSession = useCallback(async (sessionId) => {
|
|
270
|
+
if (pendingDeletedIdsRef.current.has(sessionId))
|
|
271
|
+
return false;
|
|
272
|
+
const requestId = ++sessionMutationVersionRef.current;
|
|
273
|
+
const currentList = sessionListRef.current;
|
|
274
|
+
const { next: removedList, removed, index: removedIndex } = removeSessionById(currentList, sessionId);
|
|
275
|
+
if (!removed || removedIndex < 0)
|
|
276
|
+
return false;
|
|
277
|
+
pendingDeletedIdsRef.current.add(sessionId);
|
|
278
|
+
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
279
|
+
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
280
|
+
? refs.sessionRef.current
|
|
281
|
+
: null;
|
|
282
|
+
const activeWorkspaceId = activeSession?.workspaceId ?? workspaceId;
|
|
283
|
+
const deletingRow = {
|
|
284
|
+
...removed,
|
|
285
|
+
optimisticState: "deleting",
|
|
286
|
+
};
|
|
287
|
+
const deletingList = restoreSessionAt(removedList, deletingRow, removedIndex);
|
|
288
|
+
sessionListRef.current = deletingList;
|
|
289
|
+
setSessionList(deletingList);
|
|
290
|
+
window.setTimeout(() => {
|
|
291
|
+
if (!pendingDeletedIdsRef.current.has(sessionId) ||
|
|
292
|
+
deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
293
|
+
return;
|
|
294
|
+
const next = sessionListRef.current.filter((item) => item.sessionId !== sessionId);
|
|
295
|
+
sessionListRef.current = next;
|
|
296
|
+
setSessionList(next);
|
|
297
|
+
}, 150);
|
|
298
|
+
const restore = () => {
|
|
299
|
+
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
300
|
+
return;
|
|
301
|
+
pendingDeletedIdsRef.current.delete(sessionId);
|
|
302
|
+
deleteRequestVersionsRef.current.delete(sessionId);
|
|
303
|
+
const restored = restoreSessionAt(sessionListRef.current, removed, removedIndex);
|
|
304
|
+
if (restored === sessionListRef.current)
|
|
305
|
+
return;
|
|
306
|
+
sessionListRef.current = restored;
|
|
307
|
+
setSessionList(restored);
|
|
308
|
+
};
|
|
309
|
+
let deletionSelectionVersion = null;
|
|
310
|
+
if (activeSession) {
|
|
311
|
+
deletionSelectionVersion = ++sessionSelectionVersionRef.current;
|
|
312
|
+
refs.sessionRef.current = null;
|
|
313
|
+
resetActiveProjection();
|
|
314
|
+
setSession(null);
|
|
315
|
+
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
316
|
+
}
|
|
317
|
+
const restoreActiveSession = async () => {
|
|
318
|
+
if (activeSession &&
|
|
319
|
+
deletionSelectionVersion === sessionSelectionVersionRef.current &&
|
|
320
|
+
!refs.sessionRef.current) {
|
|
321
|
+
await activateSessionRef.current?.(activeSession.sessionId, activeSession.workspaceId);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
174
324
|
try {
|
|
175
325
|
// AC-002: delete follows the session's durable owner, not the
|
|
176
326
|
// global active workspace.
|
|
@@ -184,22 +334,21 @@ export function useChatSessions(params) {
|
|
|
184
334
|
},
|
|
185
335
|
});
|
|
186
336
|
if (!res.ok) {
|
|
337
|
+
restore();
|
|
338
|
+
await restoreActiveSession();
|
|
187
339
|
setError("删除会话失败");
|
|
188
340
|
return false;
|
|
189
341
|
}
|
|
190
342
|
titleWatcherRef.current?.cancel(sessionId);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
resetActiveProjection();
|
|
196
|
-
setSession(null);
|
|
197
|
-
forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
|
|
198
|
-
}
|
|
199
|
-
await refreshSessionList();
|
|
343
|
+
void refreshSessionList().finally(() => {
|
|
344
|
+
pendingDeletedIdsRef.current.delete(sessionId);
|
|
345
|
+
deleteRequestVersionsRef.current.delete(sessionId);
|
|
346
|
+
});
|
|
200
347
|
return true;
|
|
201
348
|
}
|
|
202
349
|
catch (error) {
|
|
350
|
+
restore();
|
|
351
|
+
await restoreActiveSession();
|
|
203
352
|
setError(error instanceof Error ? error.message : String(error));
|
|
204
353
|
return false;
|
|
205
354
|
}
|
|
@@ -225,7 +374,7 @@ export function useChatSessions(params) {
|
|
|
225
374
|
if (!res.ok || !body.session)
|
|
226
375
|
throw new Error(body.error?.message ?? "会话不可用");
|
|
227
376
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
228
|
-
return;
|
|
377
|
+
return false;
|
|
229
378
|
const next = {
|
|
230
379
|
sessionId: body.session.sessionId,
|
|
231
380
|
...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
|
|
@@ -276,11 +425,13 @@ export function useChatSessions(params) {
|
|
|
276
425
|
});
|
|
277
426
|
rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
|
|
278
427
|
setActivePreviewPath(null);
|
|
428
|
+
return true;
|
|
279
429
|
}
|
|
280
430
|
catch (error) {
|
|
281
431
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
282
|
-
return;
|
|
432
|
+
return false;
|
|
283
433
|
setError(error instanceof Error ? error.message : String(error));
|
|
434
|
+
return false;
|
|
284
435
|
}
|
|
285
436
|
}, [
|
|
286
437
|
origin,
|
|
@@ -296,6 +447,7 @@ export function useChatSessions(params) {
|
|
|
296
447
|
sessionOwnerQuery,
|
|
297
448
|
workspaceId,
|
|
298
449
|
]);
|
|
450
|
+
activateSessionRef.current = activateSession;
|
|
299
451
|
useEffect(() => {
|
|
300
452
|
void refreshSessionList();
|
|
301
453
|
}, [refreshSessionList]);
|
|
@@ -437,15 +589,38 @@ export function useChatSessions(params) {
|
|
|
437
589
|
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
438
590
|
return Promise.reject(new Error("WORKSPACE_REQUIRED"));
|
|
439
591
|
}
|
|
592
|
+
const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
593
|
+
const now = new Date().toISOString();
|
|
594
|
+
const optimisticRow = {
|
|
595
|
+
sessionId: optimisticSessionId,
|
|
596
|
+
workspaceId: scope,
|
|
597
|
+
state: "active",
|
|
598
|
+
updatedAt: now,
|
|
599
|
+
turnPhase: "idle",
|
|
600
|
+
optimisticState: "creating",
|
|
601
|
+
};
|
|
602
|
+
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
603
|
+
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
604
|
+
sessionListRef.current = optimisticList;
|
|
605
|
+
setSessionList(optimisticList);
|
|
440
606
|
return createSessionAndPrimeTitleWatcher({
|
|
441
607
|
origin,
|
|
442
608
|
sessionListRef,
|
|
443
609
|
setSessionList,
|
|
444
610
|
titleWatcher: titleWatcherRef.current,
|
|
445
611
|
refreshSessionList,
|
|
446
|
-
options: { ...options, workspaceId: scope },
|
|
612
|
+
options: { ...options, workspaceId: scope, optimisticSessionId },
|
|
447
613
|
setCreatingSession,
|
|
448
614
|
creatingRef: creatingSessionRef,
|
|
615
|
+
}).then((created) => {
|
|
616
|
+
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
617
|
+
return created;
|
|
618
|
+
}).catch((error) => {
|
|
619
|
+
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
620
|
+
const cleaned = sessionListRef.current.filter((item) => item.sessionId !== optimisticSessionId);
|
|
621
|
+
sessionListRef.current = cleaned;
|
|
622
|
+
setSessionList(cleaned);
|
|
623
|
+
throw error;
|
|
449
624
|
});
|
|
450
625
|
}, [origin, refreshSessionList, workspaceId, setError]);
|
|
451
626
|
const createSession = useCallback(async (options) => {
|
|
@@ -456,11 +631,36 @@ export function useChatSessions(params) {
|
|
|
456
631
|
return null; // AC-R5-002: unknown workspace — no POST
|
|
457
632
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
458
633
|
setError(null);
|
|
634
|
+
const previousSession = refs.sessionRef.current;
|
|
635
|
+
const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
|
|
636
|
+
if (!scope) {
|
|
637
|
+
setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
// A caller-provided snapshot is an explicit new-session boundary; reusing
|
|
641
|
+
// an existing shell would silently drop that context suffix.
|
|
642
|
+
const reusable = options?.snapshot
|
|
643
|
+
? undefined
|
|
644
|
+
: findReusableBlankSession(sessionListRef.current, scope);
|
|
645
|
+
if (reusable) {
|
|
646
|
+
if (previousSession?.sessionId === reusable.sessionId)
|
|
647
|
+
return previousSession;
|
|
648
|
+
const activated = await activateSession(reusable.sessionId, reusable.workspaceId);
|
|
649
|
+
if (!activated)
|
|
650
|
+
return null;
|
|
651
|
+
return {
|
|
652
|
+
sessionId: reusable.sessionId,
|
|
653
|
+
...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
|
|
654
|
+
...runtimeSelectionFromRecord(reusable),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
459
657
|
try {
|
|
658
|
+
resetActiveProjection({ keepStreams: true });
|
|
659
|
+
refs.sessionRef.current = null;
|
|
660
|
+
setSession(null);
|
|
460
661
|
const created = await createSessionAndPrimeTitleWatcherForHook(options);
|
|
461
662
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
462
663
|
return null;
|
|
463
|
-
resetActiveProjection({ keepStreams: true });
|
|
464
664
|
// Set ref synchronously BEFORE setState so a chained
|
|
465
665
|
// createSession().then(sendPrompt) reads the new session even
|
|
466
666
|
// though sendPrompt's closure captured the stale (null) value.
|
|
@@ -472,6 +672,9 @@ export function useChatSessions(params) {
|
|
|
472
672
|
catch (e) {
|
|
473
673
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
474
674
|
return null;
|
|
675
|
+
if (previousSession) {
|
|
676
|
+
await activateSession(previousSession.sessionId, previousSession.workspaceId);
|
|
677
|
+
}
|
|
475
678
|
const message = e instanceof Error ? e.message : String(e);
|
|
476
679
|
if (message !== "CREATE_SESSION_IN_FLIGHT") {
|
|
477
680
|
setError(message);
|
|
@@ -484,6 +687,7 @@ export function useChatSessions(params) {
|
|
|
484
687
|
setError,
|
|
485
688
|
resetActiveProjection,
|
|
486
689
|
createSessionAndPrimeTitleWatcherForHook,
|
|
690
|
+
activateSession,
|
|
487
691
|
blocked,
|
|
488
692
|
workspaceId,
|
|
489
693
|
]);
|
|
@@ -28,6 +28,7 @@ export function useComposer(params) {
|
|
|
28
28
|
}, []);
|
|
29
29
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
30
30
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
31
|
+
const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
|
|
31
32
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
32
33
|
const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
|
|
33
34
|
const [commandsEpoch, setCommandsEpoch] = useState(0);
|
|
@@ -74,6 +75,7 @@ export function useComposer(params) {
|
|
|
74
75
|
commandsSessionRef.current = sessionId;
|
|
75
76
|
commandsGenerationRef.current += 1;
|
|
76
77
|
setDynamicCommands([]);
|
|
78
|
+
setCommandCapabilities({ sddBingo: false });
|
|
77
79
|
setDynamicCommandsError(null);
|
|
78
80
|
setDynamicCommandsLoading(false);
|
|
79
81
|
}, [sessionId]);
|
|
@@ -122,6 +124,7 @@ export function useComposer(params) {
|
|
|
122
124
|
if (!isCurrent())
|
|
123
125
|
return;
|
|
124
126
|
setDynamicCommands(items);
|
|
127
|
+
setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
|
|
125
128
|
// Compact server fail-soft / partial-source warning; local Operator/Web still merge.
|
|
126
129
|
const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
|
|
127
130
|
setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
|
|
@@ -131,6 +134,7 @@ export function useComposer(params) {
|
|
|
131
134
|
return;
|
|
132
135
|
setDynamicCommandsError("动态命令加载失败");
|
|
133
136
|
setDynamicCommands([]);
|
|
137
|
+
setCommandCapabilities({ sddBingo: false });
|
|
134
138
|
}
|
|
135
139
|
finally {
|
|
136
140
|
if (isCurrent()) {
|
|
@@ -227,7 +231,7 @@ export function useComposer(params) {
|
|
|
227
231
|
}, 400);
|
|
228
232
|
return () => window.clearTimeout(timer);
|
|
229
233
|
}, [origin, sessionId, input]);
|
|
230
|
-
const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands), [dynamicCommands]);
|
|
234
|
+
const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
|
|
231
235
|
const filteredShortcuts = useMemo(() => input.startsWith("/")
|
|
232
236
|
? filterSlashCommands(mergedCommands, input)
|
|
233
237
|
: [], [input, mergedCommands]);
|
|
@@ -32,6 +32,7 @@ import { ChatSessionStore } from "./chat/session-store.js";
|
|
|
32
32
|
import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
|
|
33
33
|
import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
|
|
34
34
|
import { ConsolePiRuntime } from "./chat/pi-runtime.js";
|
|
35
|
+
import { AimaxAuthReader, SddDataAlignmentService } from "./chat/sdd-data-alignment.js";
|
|
35
36
|
import { UserQuestionRegistry } from "./chat/user-questions.js";
|
|
36
37
|
import { TodoListRegistry } from "./chat/todos.js";
|
|
37
38
|
import { GoalRegistry } from "./chat/goals.js";
|
|
@@ -253,6 +254,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
253
254
|
}
|
|
254
255
|
},
|
|
255
256
|
});
|
|
257
|
+
const sddDataAlignment = new SddDataAlignmentService(chatStore, new AimaxAuthReader(), deps.fetchImpl);
|
|
256
258
|
const chatEventEpoch = randomBytes(12).toString("hex");
|
|
257
259
|
const chatEvents = createChatEventStore({
|
|
258
260
|
appData,
|
|
@@ -291,6 +293,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
291
293
|
questionRegistry,
|
|
292
294
|
todoRegistry,
|
|
293
295
|
goalRegistry,
|
|
296
|
+
sddDataAlignment,
|
|
294
297
|
});
|
|
295
298
|
let chatRouteDeps;
|
|
296
299
|
const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
|
|
@@ -302,6 +305,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
302
305
|
questionRegistry,
|
|
303
306
|
todoRegistry,
|
|
304
307
|
goalRegistry,
|
|
308
|
+
sddDataAlignment,
|
|
305
309
|
goalRoundDriver,
|
|
306
310
|
operationLinker: chatOperationLinker,
|
|
307
311
|
appData,
|
|
@@ -1463,7 +1463,7 @@ for top in ast.walk(tree):
|
|
|
1463
1463
|
returned_names.add(node.value.id)
|
|
1464
1464
|
if isinstance(node,ast.Call):
|
|
1465
1465
|
for keyword in node.keywords:
|
|
1466
|
-
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in positional:
|
|
1466
|
+
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body','params','query','query_params'} and isinstance(keyword.value,ast.Name) and keyword.value.id in positional:
|
|
1467
1467
|
transport.append((keyword.value.id,positional.index(keyword.value.id)-call_offset))
|
|
1468
1468
|
if isinstance(node.func,ast.Attribute) and node.func.attr.lower() in {'post','put','patch'} and len(node.args) >= 2:
|
|
1469
1469
|
candidate=node.args[1]
|
|
@@ -1484,7 +1484,7 @@ for top in ast.walk(tree):
|
|
|
1484
1484
|
for node in ast.walk(top):
|
|
1485
1485
|
if not isinstance(node,ast.Call): continue
|
|
1486
1486
|
for keyword in node.keywords:
|
|
1487
|
-
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body'} and isinstance(keyword.value,ast.Name) and keyword.value.id in assigned:
|
|
1487
|
+
if keyword.arg in {'payload','body','request_body','request_json','dto','json','data','json_body','params','query','query_params'} and isinstance(keyword.value,ast.Name) and keyword.value.id in assigned:
|
|
1488
1488
|
transport_shapes.append(assigned[keyword.value.id])
|
|
1489
1489
|
if candidates: FUNCTION_PAYLOADS[top.name]=candidates
|
|
1490
1490
|
if transport_shapes: FUNCTION_TRANSPORT_SHAPES[top.name]=transport_shapes
|
|
@@ -1540,8 +1540,9 @@ def literal(node):
|
|
|
1540
1540
|
return repr(node.value)
|
|
1541
1541
|
return None
|
|
1542
1542
|
|
|
1543
|
-
def collect_dict(node, prefix, paths, values):
|
|
1543
|
+
def collect_dict(node, prefix, paths, values, parameters=None):
|
|
1544
1544
|
if not isinstance(node, ast.Dict): return
|
|
1545
|
+
parameters = parameters or {}
|
|
1545
1546
|
for key,value in zip(node.keys,node.values):
|
|
1546
1547
|
if not isinstance(key, ast.Constant) or not isinstance(key.value,str): continue
|
|
1547
1548
|
# Ignore writer helper/meta keys that are not DTO request fields.
|
|
@@ -1551,12 +1552,16 @@ def collect_dict(node, prefix, paths, values):
|
|
|
1551
1552
|
paths.add(current)
|
|
1552
1553
|
lv=literal(value)
|
|
1553
1554
|
if lv is not None: values.setdefault(current,set()).add(lv)
|
|
1554
|
-
|
|
1555
|
+
elif isinstance(value, ast.Name) and value.id in parameters:
|
|
1556
|
+
for bound in parameters[value.id]:
|
|
1557
|
+
bound_lv=literal(bound)
|
|
1558
|
+
if bound_lv is not None: values.setdefault(current,set()).add(bound_lv)
|
|
1559
|
+
if isinstance(value, ast.Dict): collect_dict(value,current,paths,values,parameters)
|
|
1555
1560
|
elif isinstance(value,(ast.List,ast.Tuple)):
|
|
1556
1561
|
for item in value.elts:
|
|
1557
|
-
if isinstance(item,ast.Dict): collect_dict(item,current,paths,values)
|
|
1562
|
+
if isinstance(item,ast.Dict): collect_dict(item,current,paths,values,parameters)
|
|
1558
1563
|
|
|
1559
|
-
PAYLOAD_NAMES={'payload','body','request_body','request_json','dto','json','data','json_body'}
|
|
1564
|
+
PAYLOAD_NAMES={'payload','body','request_body','request_json','dto','json','data','json_body','params','query','query_params'}
|
|
1560
1565
|
|
|
1561
1566
|
def shape_only(node):
|
|
1562
1567
|
if isinstance(node,ast.Dict):
|
|
@@ -1637,6 +1642,7 @@ def decorator_payload_nodes(parameters,assigned):
|
|
|
1637
1642
|
|
|
1638
1643
|
def decorator_payload_nodes_by_tp(fn,assigned):
|
|
1639
1644
|
result={}
|
|
1645
|
+
function_payload_nodes=payload_nodes(fn)
|
|
1640
1646
|
for dec in fn.decorator_list:
|
|
1641
1647
|
if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
|
|
1642
1648
|
if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
|
|
@@ -1653,9 +1659,11 @@ def decorator_payload_nodes_by_tp(fn,assigned):
|
|
|
1653
1659
|
parameters={}
|
|
1654
1660
|
for index,name in enumerate(names):
|
|
1655
1661
|
if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
|
|
1656
|
-
|
|
1662
|
+
payload_nodes_for_tp=decorator_payload_nodes(parameters,assigned)
|
|
1663
|
+
if not payload_nodes_for_tp:
|
|
1664
|
+
payload_nodes_for_tp=function_payload_nodes
|
|
1657
1665
|
paths=set(); values={}
|
|
1658
|
-
for payload_node in
|
|
1666
|
+
for payload_node in payload_nodes_for_tp: collect_dict(payload_node,'',paths,values,parameters)
|
|
1659
1667
|
result[tp]={key:sorted(items) for key,items in sorted(values.items())}
|
|
1660
1668
|
return result
|
|
1661
1669
|
|
|
@@ -1729,8 +1737,8 @@ for fn in ast.walk(tree):
|
|
|
1729
1737
|
if isinstance(fn,(ast.FunctionDef,ast.AsyncFunctionDef)) and fn.name.startswith('test_'):
|
|
1730
1738
|
paths=set(); values={}
|
|
1731
1739
|
fallback_paths=set(); fallback_values={}
|
|
1732
|
-
for node in payload_nodes(fn): collect_dict(node,'',paths,values)
|
|
1733
|
-
for node in fallback_payload_nodes(fn): collect_dict(node,'',fallback_paths,fallback_values)
|
|
1740
|
+
for node in payload_nodes(fn): collect_dict(node,'',paths,values,{})
|
|
1741
|
+
for node in fallback_payload_nodes(fn): collect_dict(node,'',fallback_paths,fallback_values,{})
|
|
1734
1742
|
out[fn.name]={
|
|
1735
1743
|
'paths':sorted(paths),
|
|
1736
1744
|
'values':{key:sorted(items) for key,items in sorted(values.items())},
|
|
@@ -1782,14 +1790,39 @@ function metadataTestPoints(region, label) {
|
|
|
1782
1790
|
const match = new RegExp(`^\\s*${escaped}\\s*:\\s*(.*)$`, "mi").exec(region);
|
|
1783
1791
|
return orderedUnique(match?.[1]?.match(/\bTP-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g) ?? []);
|
|
1784
1792
|
}
|
|
1793
|
+
function isPytestDecoratorOrContinuationLine(line) {
|
|
1794
|
+
const trimmed = line.trim();
|
|
1795
|
+
if (!trimmed)
|
|
1796
|
+
return false;
|
|
1797
|
+
if (trimmed.startsWith("#"))
|
|
1798
|
+
return true;
|
|
1799
|
+
if (trimmed.startsWith("@"))
|
|
1800
|
+
return true;
|
|
1801
|
+
// Multi-line decorator argument lists: @pytest.mark.parametrize(\n "x", [\n pytest.param(..., id="TP-...")\n ]\n)
|
|
1802
|
+
return /^[\]\),]/.test(trimmed) || /[\(\[{,]$/.test(trimmed) || /\bpytest\.param\s*\(/.test(trimmed);
|
|
1803
|
+
}
|
|
1785
1804
|
function pytestFunctionRegionStart(source, functionIndex) {
|
|
1786
1805
|
let regionStart = source.lastIndexOf("\n", functionIndex - 1) + 1;
|
|
1787
1806
|
let cursor = regionStart;
|
|
1807
|
+
let blankRun = 0;
|
|
1788
1808
|
while (cursor > 0) {
|
|
1789
1809
|
const previousEnd = cursor - 1;
|
|
1790
1810
|
const previousStart = source.lastIndexOf("\n", previousEnd - 1) + 1;
|
|
1791
1811
|
const previous = source.slice(previousStart, previousEnd).replace(/\r$/, "");
|
|
1792
|
-
|
|
1812
|
+
const trimmed = previous.trim();
|
|
1813
|
+
if (!trimmed) {
|
|
1814
|
+
blankRun += 1;
|
|
1815
|
+
// Keep walking across a small blank gap so a decorator is still bound to
|
|
1816
|
+
// the following test when writers insert one empty line before `def`.
|
|
1817
|
+
if (blankRun > 2)
|
|
1818
|
+
break;
|
|
1819
|
+
cursor = previousStart;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
blankRun = 0;
|
|
1823
|
+
if (/^(?:async\s+)?def\s+/.test(trimmed) || /^(?:async\s+)?class\s+/.test(trimmed))
|
|
1824
|
+
break;
|
|
1825
|
+
if (!isPytestDecoratorOrContinuationLine(previous))
|
|
1793
1826
|
break;
|
|
1794
1827
|
regionStart = previousStart;
|
|
1795
1828
|
cursor = previousStart;
|
|
@@ -1819,12 +1852,14 @@ function pytestFunctionBodyEnd(source, signatureEnd, functionIndent) {
|
|
|
1819
1852
|
}
|
|
1820
1853
|
function pytestParameterCollections(source) {
|
|
1821
1854
|
const collections = new Map();
|
|
1822
|
-
for (const match of source.matchAll(/^(
|
|
1855
|
+
for (const match of source.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\[(?![^\r\n]*\])([\s\S]*?)^\]/gm)) {
|
|
1823
1856
|
const explicitIds = [...match[2].matchAll(/\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1]);
|
|
1824
1857
|
const factoryIds = explicitIds.length === 0
|
|
1825
1858
|
? [...match[2].matchAll(/["'](TP-[A-Z0-9-]+)["']/g)].map((item) => item[1])
|
|
1826
1859
|
: [];
|
|
1827
|
-
|
|
1860
|
+
const ids = orderedUnique([...explicitIds, ...factoryIds]);
|
|
1861
|
+
if (ids.length > 0)
|
|
1862
|
+
collections.set(match[1], ids);
|
|
1828
1863
|
}
|
|
1829
1864
|
return collections;
|
|
1830
1865
|
}
|
|
@@ -1886,6 +1921,12 @@ function normalizePythonLiteralValue(value) {
|
|
|
1886
1921
|
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
1887
1922
|
return trimmed.slice(1, -1);
|
|
1888
1923
|
}
|
|
1924
|
+
if (trimmed === "True")
|
|
1925
|
+
return "true";
|
|
1926
|
+
if (trimmed === "False")
|
|
1927
|
+
return "false";
|
|
1928
|
+
if (trimmed === "None")
|
|
1929
|
+
return "null";
|
|
1889
1930
|
return trimmed;
|
|
1890
1931
|
}
|
|
1891
1932
|
function intentionalInvalidEnumVariant(input) {
|
|
@@ -1930,13 +1971,15 @@ function assessPayloadContract(testCase, refs) {
|
|
|
1930
1971
|
: [];
|
|
1931
1972
|
const enumMismatches = [];
|
|
1932
1973
|
for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
|
|
1933
|
-
const
|
|
1974
|
+
const aggregated = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
|
|
1975
|
+
const byTestPoint = orderedUnique(refs.flatMap((item) => Object.values(item.payloadValuesByTestPoint).flatMap((values) => values[field] ?? [])));
|
|
1976
|
+
const observed = orderedUnique([...aggregated, ...byTestPoint]);
|
|
1934
1977
|
for (const rawValue of observed) {
|
|
1935
1978
|
const value = normalizePythonLiteralValue(rawValue);
|
|
1936
1979
|
if (allowed.includes(value))
|
|
1937
1980
|
continue;
|
|
1938
1981
|
const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
|
|
1939
|
-
.filter(([, values]) => (values[field] ?? []).
|
|
1982
|
+
.filter(([, values]) => (values[field] ?? []).some((itemValue) => itemValue === rawValue || normalizePythonLiteralValue(itemValue) === value))
|
|
1940
1983
|
.map(([testPoint]) => testPoint)));
|
|
1941
1984
|
const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
|
|
1942
1985
|
if (!intentionallyInvalid)
|