@cspeach/cli 1.1.34 → 1.1.36
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/.gitattributes +1 -0
- package/dist/agent/loop.js +127 -31
- package/dist/agent/parallel-write-guard.js +3 -1
- package/dist/commands/plan-audit-evidence.js +6 -1
- package/dist/commands/plan-gate.js +10 -1
- package/dist/commands/plan-resume.js +9 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/one-shot.js +3 -0
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/projects/extract-plan.js +6 -0
- package/dist/projects/plan-objects.js +1 -1
- package/dist/projects/plan-skeleton.js +93 -0
- package/dist/projects/save-command.js +17 -0
- package/dist/rap/finish-checks.js +389 -0
- package/dist/rap/generator-names.js +57 -0
- package/dist/rap/skeleton-card.js +31 -0
- package/dist/rap/skeleton-rules.js +38 -0
- package/dist/rap/skeleton-session.js +21 -0
- package/dist/rap/skeleton-store.js +40 -0
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/tool-labels.js +12 -3
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/rule8-detector.js +3 -2
- package/dist/repl.js +11 -2
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/generator-fact.js +21 -0
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +19 -1
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/rap/finish-check.js +118 -0
- package/dist/tools/rap/generate-objects.js +317 -0
- package/dist/tools/rap/skeleton-resolve.js +283 -0
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +7 -1
- package/dist/tools/write-mode.js +2 -0
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/package.json +2 -2
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// cspeach-cli/src/tools/rap/skeleton-resolve.ts
|
|
2
|
+
//
|
|
3
|
+
// RAP generator plan, Task 7 — `rap_skeleton_resolve`. Spec 2026-10-14 §3.2/§4:
|
|
4
|
+
// the CLI picks the skeleton builder (SAP's generator or the CSPeach writer),
|
|
5
|
+
// shows the skeleton card, writes the sha-pinned decision file and returns the
|
|
6
|
+
// block the plan copies. The model never picks.
|
|
7
|
+
//
|
|
8
|
+
// Every SAP read here is best-effort: a read that throws (other than the
|
|
9
|
+
// table's own 404) makes the generator fact 'unknown', so the CSPeach writer
|
|
10
|
+
// is chosen (row 3). The tool never crashes on a SAP read.
|
|
11
|
+
import { SapError } from '@cspeach/sap-client';
|
|
12
|
+
import { isStandalone, registerTool } from '../index.js';
|
|
13
|
+
import { assertNotCliHome, laneRootFor } from '../../lane/roots.js';
|
|
14
|
+
import { setLatestSkeletonDecision } from '../../rap/skeleton-session.js';
|
|
15
|
+
import { factsFromSystemInfo } from '../../lane/inputs.js';
|
|
16
|
+
import { cardLine } from '../../lane/card.js';
|
|
17
|
+
import { getSapSystemInfo } from '../../sap/system-info.js';
|
|
18
|
+
import { getGeneratorFact } from '../../sap/generator-fact.js';
|
|
19
|
+
import { decideSkeleton, generatorBlockers, } from '../../rap/skeleton-rules.js';
|
|
20
|
+
import { buildUiserviceContent, generatorPlannedObjects, namesFitLimits, proposeNames, } from '../../rap/generator-names.js';
|
|
21
|
+
import { writeSkeletonDecision } from '../../rap/skeleton-store.js';
|
|
22
|
+
import { SKELETON_WHO, skeletonCardRequest } from '../../rap/skeleton-card.js';
|
|
23
|
+
import { PKG_RE } from '../../projects/plan-objects.js';
|
|
24
|
+
import { askConfirm } from '../../ui/confirm-request.js';
|
|
25
|
+
import { cardSlotEmitter } from '../../ui/card-slot.js';
|
|
26
|
+
import { isHeadless } from '../../renderer/tty.js';
|
|
27
|
+
const fail = (m) => ({ content: `error: ${m}`, is_error: true });
|
|
28
|
+
const NOT_CHOSEN = 'Skeleton not chosen. Nothing was saved.';
|
|
29
|
+
const TABLE_RE = /^[ZY][A-Z0-9_]{1,15}$/;
|
|
30
|
+
const BINDINGS = new Set(['odata-v4-ui', 'odata-v2-ui', 'odata-v4-api', 'odata-v2-api']);
|
|
31
|
+
const RAP_TIER = (t) => t === 's4-2020-plus' || t === 'unknown';
|
|
32
|
+
const TS_FIELDS = [/:\s*abp_locinst_lastchange_tstmpl\b/i, /:\s*abp_lastchange_tstmpl\b/i];
|
|
33
|
+
function parseRequest(r) {
|
|
34
|
+
if (!r || typeof r !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
const o = r;
|
|
37
|
+
if (typeof o.entities !== 'number' || !Number.isInteger(o.entities) || o.entities < 1)
|
|
38
|
+
return null;
|
|
39
|
+
if (o.implementation !== 'managed' && o.implementation !== 'unmanaged')
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof o.draft !== 'boolean' || typeof o.binding !== 'string' || !BINDINGS.has(o.binding))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
entities: o.entities, implementation: o.implementation, draft: o.draft,
|
|
45
|
+
binding: o.binding,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function defaultTier(ctx) {
|
|
49
|
+
return { tier: factsFromSystemInfo(await getSapSystemInfo(ctx.adt, ctx.sapAlias)).tier };
|
|
50
|
+
}
|
|
51
|
+
function defaultHeadless() {
|
|
52
|
+
return isHeadless() || cardSlotEmitter.listenerCount('confirm') === 0;
|
|
53
|
+
}
|
|
54
|
+
const SUFFIXES = ['', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
55
|
+
const RENAMED_NOTE = 'Some names were taken, so CSPeach picked free ones';
|
|
56
|
+
const takenMessage = (name) => `${name} already exists in the system. Choose another table or app name, then run /abap-plan again.`;
|
|
57
|
+
/** The name a SAP validation text says is taken, when it is one of ours. */
|
|
58
|
+
function clashInValidation(text, objects) {
|
|
59
|
+
if (!/already exist/i.test(text))
|
|
60
|
+
return null;
|
|
61
|
+
const up = text.toUpperCase();
|
|
62
|
+
const hit = objects.find((o) => {
|
|
63
|
+
const at = up.indexOf(o.name);
|
|
64
|
+
if (at < 0)
|
|
65
|
+
return false;
|
|
66
|
+
const before = at === 0 ? '' : up[at - 1];
|
|
67
|
+
const after = up[at + o.name.length] ?? '';
|
|
68
|
+
return !/[A-Z0-9_]/.test(before) && !/[A-Z0-9_]/.test(after);
|
|
69
|
+
});
|
|
70
|
+
return hit ? hit.name : null;
|
|
71
|
+
}
|
|
72
|
+
/** First of `objects` that already exists (exact name), or null. Throws what searchObject throws. */
|
|
73
|
+
async function searchClash(ctx, objects) {
|
|
74
|
+
for (const o of objects) {
|
|
75
|
+
const hits = await ctx.adt.searchObject(o.name, o.type, 5);
|
|
76
|
+
if (hits.some((h) => String(h.name).toUpperCase() === o.name))
|
|
77
|
+
return o.name;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Fills the table facts (rows 5–7) from SAP for one candidate name set and
|
|
83
|
+
* returns the name of one of OUR objects that is already taken, or null.
|
|
84
|
+
* Throws only what the caller maps to generator 'unknown'; a 404 on the table
|
|
85
|
+
* means "not created yet".
|
|
86
|
+
*/
|
|
87
|
+
async function probeTable(ctx, facts, names, objects9, content, cache) {
|
|
88
|
+
if (cache.src === undefined) {
|
|
89
|
+
try {
|
|
90
|
+
cache.src = await ctx.adt.getSource('TABL', names.table);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
if (!(e instanceof SapError && e.httpStatus === 404))
|
|
94
|
+
throw e;
|
|
95
|
+
cache.src = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (cache.src !== null) {
|
|
99
|
+
const text = cache.src;
|
|
100
|
+
facts.tableExists = true;
|
|
101
|
+
facts.tableHasTimestamps = TS_FIELDS.every((re) => re.test(text));
|
|
102
|
+
if (!facts.tableHasTimestamps)
|
|
103
|
+
return null;
|
|
104
|
+
if (cache.ui === undefined) {
|
|
105
|
+
const list = await ctx.adt.listGenerators(`/sap/bc/adt/ddic/tables/${names.table.toLowerCase()}`);
|
|
106
|
+
cache.ui = list.find((g) => g.id === 'uiservice') ?? null;
|
|
107
|
+
}
|
|
108
|
+
facts.uiserviceOffered = !!cache.ui;
|
|
109
|
+
if (cache.ui) {
|
|
110
|
+
const v = await ctx.adt.validateGenerator(cache.ui, content);
|
|
111
|
+
// Final review I3 — a clash on one of our names is renamed, not a row-7 stop.
|
|
112
|
+
const clash = v.ok ? null : clashInValidation(v.text, objects9);
|
|
113
|
+
if (clash)
|
|
114
|
+
return clash;
|
|
115
|
+
facts.validation = { ok: v.ok, text: v.text };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
// Not created yet: SAP's own validation needs the table, so check the 9 names here.
|
|
120
|
+
return searchClash(ctx, objects9);
|
|
121
|
+
}
|
|
122
|
+
/** The objects the CSPeach writer builds under the record's names (no draft table without draft). */
|
|
123
|
+
function writerObjects(objects9, request) {
|
|
124
|
+
return request.draft ? objects9 : objects9.filter((o) => o.type !== 'TABL');
|
|
125
|
+
}
|
|
126
|
+
export async function rapSkeletonResolveHandler(args, ctx, deps = {}) {
|
|
127
|
+
// 1. Arguments.
|
|
128
|
+
const table = String(args?.table ?? '').trim().toUpperCase();
|
|
129
|
+
if (!TABLE_RE.test(table))
|
|
130
|
+
return fail('table must be a Z/Y table name of at most 16 characters.');
|
|
131
|
+
const pkg = String(args?.package ?? '').trim().toUpperCase();
|
|
132
|
+
if (!PKG_RE.test(pkg))
|
|
133
|
+
return fail('package must be a valid package name.');
|
|
134
|
+
const request = parseRequest(args?.request);
|
|
135
|
+
if (!request) {
|
|
136
|
+
return fail('request must be { entities, implementation: managed|unmanaged, draft, binding: odata-v4-ui|odata-v2-ui|odata-v4-api|odata-v2-api }.');
|
|
137
|
+
}
|
|
138
|
+
const description = (String(args?.description ?? '').trim() || table).slice(0, 60);
|
|
139
|
+
// 2. System facts. Final review I1 — standalone (or no SAP client): no SAP read at all.
|
|
140
|
+
const connected = !isStandalone(ctx) && !!ctx.adt;
|
|
141
|
+
const { tier } = connected
|
|
142
|
+
? await (deps.facts ?? defaultTier)(ctx).catch(() => ({ tier: 'unknown' }))
|
|
143
|
+
: { tier: 'unknown' };
|
|
144
|
+
let generator = connected
|
|
145
|
+
? await (deps.generatorFact ?? ((c) => getGeneratorFact(c.adt, c.sapAlias)))(ctx)
|
|
146
|
+
.catch(() => 'unknown')
|
|
147
|
+
: 'unknown';
|
|
148
|
+
// 3–4. Names, then the table facts. Final review I3 — a taken name is never
|
|
149
|
+
// handed on: each candidate set (base, base2 … base9) is checked the same way
|
|
150
|
+
// and the first free set wins. When SAP can't be searched the names stay unchecked.
|
|
151
|
+
const cache = {};
|
|
152
|
+
let chosen = null;
|
|
153
|
+
let firstTaken = null;
|
|
154
|
+
for (const suffix of SUFFIXES) {
|
|
155
|
+
const names = proposeNames(table, suffix);
|
|
156
|
+
const facts = {
|
|
157
|
+
tier, generator, tableExists: false, tableHasTimestamps: null, uiserviceOffered: null, validation: null,
|
|
158
|
+
namesFit: namesFitLimits(names), connected, localPackage: pkg.startsWith('$'),
|
|
159
|
+
};
|
|
160
|
+
const objects9 = generatorPlannedObjects(names, pkg);
|
|
161
|
+
const content = buildUiserviceContent(names, pkg, description);
|
|
162
|
+
let taken = null;
|
|
163
|
+
// Only when the generator could still be chosen.
|
|
164
|
+
if (RAP_TIER(tier) && generator === 'yes' && !generatorBlockers(request, facts)) {
|
|
165
|
+
try {
|
|
166
|
+
taken = await probeTable(ctx, facts, names, objects9, content, cache);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
generator = 'unknown';
|
|
170
|
+
facts.generator = 'unknown';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// The CSPeach writer builds the same names: check them when it is the one deciding.
|
|
174
|
+
if (!taken && connected && RAP_TIER(tier) && generatorBlockers(request, facts)) {
|
|
175
|
+
try {
|
|
176
|
+
taken = await searchClash(ctx, writerObjects(objects9, request));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
taken = null; // SAP can't be searched: the names stay unchecked (the build reports a clash).
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!taken) {
|
|
183
|
+
chosen = { names, facts, objects9, content };
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
firstTaken ??= taken;
|
|
187
|
+
}
|
|
188
|
+
if (!chosen)
|
|
189
|
+
return { content: takenMessage(firstTaken ?? table) };
|
|
190
|
+
const { names, facts, objects9, content } = chosen;
|
|
191
|
+
const renamed = names.base !== proposeNames(table).base;
|
|
192
|
+
const noted = (d) => (renamed && d.by !== null ? { ...d, reason: `${d.reason}. ${RENAMED_NOTE}` } : d);
|
|
193
|
+
// 5. Decide.
|
|
194
|
+
let decision = noted(decideSkeleton(request, facts));
|
|
195
|
+
if (decision.by === null)
|
|
196
|
+
return { content: `${decision.reason}. Nothing was saved.` };
|
|
197
|
+
const blocked = generatorBlockers(request, facts);
|
|
198
|
+
// 6. Card, or the rules when no card can be shown.
|
|
199
|
+
if ((deps.headless ?? defaultHeadless)()) {
|
|
200
|
+
ctx.chunkEmitter?.emit('info', `RAP skeleton chosen by the rules (no confirm card here): ${SKELETON_WHO[decision.by]}.`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const card = skeletonCardRequest(decision, blocked?.reason ?? null, names, objects9);
|
|
204
|
+
const res = await (deps.confirm ?? ((r) => askConfirm(r)))(card);
|
|
205
|
+
const value = res.value;
|
|
206
|
+
const offered = new Set(card.choices.map((c) => c.value));
|
|
207
|
+
if (!value || value === 'cancel' || !offered.has(value))
|
|
208
|
+
return { content: NOT_CHOSEN };
|
|
209
|
+
if (value.startsWith('switch:')) {
|
|
210
|
+
decision = noted(decideSkeleton(request, facts, value.slice('switch:'.length)));
|
|
211
|
+
}
|
|
212
|
+
else if (value !== 'confirm') {
|
|
213
|
+
return { content: NOT_CHOSEN };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const by = decision.by;
|
|
217
|
+
if (by === null)
|
|
218
|
+
return { content: NOT_CHOSEN };
|
|
219
|
+
// 7. The decision file.
|
|
220
|
+
const objects = by === 'sap-generator' ? objects9 : [];
|
|
221
|
+
let written;
|
|
222
|
+
try {
|
|
223
|
+
const laneRoot = await laneRootFor(ctx.cwd);
|
|
224
|
+
await assertNotCliHome(laneRoot);
|
|
225
|
+
written = await writeSkeletonDecision(laneRoot, {
|
|
226
|
+
table, package: pkg, names, request, decision, objects,
|
|
227
|
+
...(by === 'sap-generator' ? { content } : {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return fail(`the skeleton decision could not be saved: ${cardLine(e instanceof Error ? e.message : 'write failed')}`);
|
|
232
|
+
}
|
|
233
|
+
// Task 8 fix round 1 — the plan save accepts only this, the newest decision.
|
|
234
|
+
if (ctx.session)
|
|
235
|
+
setLatestSkeletonDecision(ctx.session, written);
|
|
236
|
+
// 8. The result.
|
|
237
|
+
const block = { skeleton: { by, decision: { path: written.path, sha256: written.sha256 } }, plannedObjects: objects };
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
`Skeleton: ${SKELETON_WHO[by]} — ${cardLine(decision.reason)}`,
|
|
241
|
+
`Decision file: ${written.path}`,
|
|
242
|
+
`Plan block: ${JSON.stringify(block)}`,
|
|
243
|
+
// Final review I3 — the writer builds the record's names (checked free, renamed when taken).
|
|
244
|
+
...(by === 'cspeach'
|
|
245
|
+
? [`Names: ${[names.root, names.projection, names.behaviorPool, ...(request.draft ? [names.draftTable] : []), names.serviceBinding].join(', ')} `
|
|
246
|
+
+ '(service definition and binding). Use exactly these names for the skeleton objects.']
|
|
247
|
+
: []),
|
|
248
|
+
"Copy the plan block into the skeleton phase's work exactly as given. Do not change it.",
|
|
249
|
+
].join('\n'),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
registerTool({
|
|
253
|
+
name: 'rap_skeleton_resolve',
|
|
254
|
+
description: "RAP apps only, at plan time. Decides how the RAP skeleton is built: SAP's generator or the CSPeach writer. "
|
|
255
|
+
+ 'Pass the persistence table name (it may not exist yet), the package, a short description and the request '
|
|
256
|
+
+ '{ entities (number of entities incl. children), implementation managed|unmanaged, draft, binding '
|
|
257
|
+
+ 'odata-v4-ui|odata-v2-ui|odata-v4-api|odata-v2-api }. CSPeach checks the system and the table, shows the '
|
|
258
|
+
+ "skeleton card, saves the decision and returns a Plan block. Copy that block into the skeleton phase's "
|
|
259
|
+
+ 'work exactly. Never choose the builder yourself.',
|
|
260
|
+
isMutating: false,
|
|
261
|
+
// Final review I1 — usable standalone: no SAP reads, the CSPeach writer ("Not connected to SAP").
|
|
262
|
+
requiresSap: false,
|
|
263
|
+
input_schema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
table: { type: 'string', description: 'Persistence table name (Z/Y, at most 16 characters)' },
|
|
267
|
+
package: { type: 'string', description: 'Target package' },
|
|
268
|
+
description: { type: 'string', description: 'Short business description, e.g. "Equipment downtime log"' },
|
|
269
|
+
request: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
entities: { type: 'integer', minimum: 1 },
|
|
273
|
+
implementation: { type: 'string', enum: ['managed', 'unmanaged'] },
|
|
274
|
+
draft: { type: 'boolean' },
|
|
275
|
+
binding: { type: 'string', enum: ['odata-v4-ui', 'odata-v2-ui', 'odata-v4-api', 'odata-v2-api'] },
|
|
276
|
+
},
|
|
277
|
+
required: ['entities', 'implementation', 'draft', 'binding'],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ['table', 'package', 'description', 'request'],
|
|
281
|
+
},
|
|
282
|
+
handler: (args, ctx) => rapSkeletonResolveHandler(args, ctx),
|
|
283
|
+
});
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -644,29 +644,29 @@ registerTool({
|
|
|
644
644
|
handler: async (args, ctx) => {
|
|
645
645
|
// runUnitTest(type, name) — type first
|
|
646
646
|
const result = await ctx.adt.runUnitTest(args.type, args.name);
|
|
647
|
-
// B2 false-green guard
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
// a
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
const summary = result
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
647
|
+
// B2 false-green guard: a run in which no test actually ran reads to a
|
|
648
|
+
// caller like "nothing failed → green". Wrap the (still-carried) raw result
|
|
649
|
+
// in an explicit UNVERIFIED envelope when:
|
|
650
|
+
// - total is 0, missing or not a number — even when programs are present
|
|
651
|
+
// (a class with no methods, or only tolerable class alerts; review I1);
|
|
652
|
+
// - every test was skipped (total > 0, passed 0, failed 0; review M1).
|
|
653
|
+
// It is no longer blamed on an ADT gap: the 2026-10-12 sap-client fix sends
|
|
654
|
+
// the request shape Eclipse sends. A run with a pass or a failure passes
|
|
655
|
+
// through unchanged.
|
|
656
|
+
const summary = result
|
|
657
|
+
?.summary;
|
|
658
|
+
const total = summary?.total;
|
|
659
|
+
const noTestsRun = typeof total !== 'number' || !Number.isFinite(total) || total === 0;
|
|
660
|
+
const allSkipped = !noTestsRun && summary?.passed === 0 && summary?.failed === 0;
|
|
661
|
+
if (noTestsRun || allSkipped) {
|
|
662
|
+
const n = total;
|
|
663
|
+
const warning = noTestsRun
|
|
664
|
+
? '0 tests ran — this is not a pass. Check that the object has an active test ' +
|
|
665
|
+
'class (FOR TESTING) with active test methods, then run it again.'
|
|
666
|
+
: `${n === 1 ? '1 test was' : `${n} tests were`} skipped and none ran — this is not ` +
|
|
667
|
+
'a pass. Check the risk level and duration of the test classes, then run it again.';
|
|
660
668
|
return {
|
|
661
|
-
content: JSON.stringify({
|
|
662
|
-
verification: 'UNVERIFIED',
|
|
663
|
-
warning: 'ABAP Unit run enumerated 0 tests (empty runResult). This does NOT mean the tests ' +
|
|
664
|
-
'passed — it means none executed. A FOR TESTING class that is active + syntax-clean ' +
|
|
665
|
-
'still returning 0 is a known ADT AUnit-run gap (project_aunit_run_gap): the ' +
|
|
666
|
-
'authoritative check is Eclipse "Run As → ABAP Unit" (Ctrl+Shift+F10). Do NOT treat ' +
|
|
667
|
-
'this as a green test gate.',
|
|
668
|
-
result,
|
|
669
|
-
}, null, 2),
|
|
669
|
+
content: JSON.stringify({ verification: 'UNVERIFIED', warning, result }, null, 2),
|
|
670
670
|
};
|
|
671
671
|
}
|
|
672
672
|
return { content: JSON.stringify(result, null, 2) };
|