cgraphx 1.2.0 → 1.3.1
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/dist/.claude-template/hooks/precommit-check/precommit-check.cjs +90 -0
- package/dist/.claude-template/skills/cgraphx-guide/how-to-use.html +100 -78
- package/dist/.claude-template/skills/precommit-review/SKILL.md +50 -0
- package/dist/.claude-template/skills/run-api-test/SKILL.md +187 -0
- package/dist/.claude-template/skills/run-api-test/assets/template-test-report.md +103 -0
- package/dist/.claude-template/skills/run-api-test/assets/template-test-verify.jsonl +5 -0
- package/dist/.claude-template/skills/run-api-test/references/bru-run.md +60 -0
- package/dist/.claude-template/skills/run-api-test/references/db-verification.md +81 -0
- package/dist/.claude-template/skills/run-api-test/references/report-format.md +104 -0
- package/dist/.claude-template/skills/run-api-test/references/service-readiness.md +61 -0
- package/dist/.claude-template/skills/run-api-test/references/test-scope.md +64 -0
- package/dist/.claude-template/skills/write-api/SKILL.md +150 -0
- package/dist/.claude-template/skills/write-api/assets/template-api-spec.md +112 -0
- package/dist/.claude-template/skills/write-api/assets/template-request.bru +72 -0
- package/dist/.claude-template/skills/write-api/references/ai-prompts.md +133 -0
- package/dist/.claude-template/skills/write-api/references/api-spec-format.md +108 -0
- package/dist/.claude-template/skills/write-api/references/bru-format.md +139 -0
- package/dist/.claude-template/skills/write-api/references/collection-layout.md +90 -0
- package/dist/.claude-template/skills/write-api/references/environment-setup.md +105 -0
- package/dist/.claude-template/skills/write-api/references/interface-scope.md +74 -0
- package/dist/api-test/ai-fields.d.ts +37 -0
- package/dist/api-test/ai-fields.d.ts.map +1 -0
- package/dist/api-test/ai-fields.js +114 -0
- package/dist/api-test/ai-fields.js.map +1 -0
- package/dist/api-test/assemble.d.ts +76 -0
- package/dist/api-test/assemble.d.ts.map +1 -0
- package/dist/api-test/assemble.js +185 -0
- package/dist/api-test/assemble.js.map +1 -0
- package/dist/api-test/bru-cli-invoker.d.ts +72 -0
- package/dist/api-test/bru-cli-invoker.d.ts.map +1 -0
- package/dist/api-test/bru-cli-invoker.js +169 -0
- package/dist/api-test/bru-cli-invoker.js.map +1 -0
- package/dist/api-test/bru-report-parser.d.ts +24 -0
- package/dist/api-test/bru-report-parser.d.ts.map +1 -0
- package/dist/api-test/bru-report-parser.js +110 -0
- package/dist/api-test/bru-report-parser.js.map +1 -0
- package/dist/api-test/bru-runner.d.ts +101 -0
- package/dist/api-test/bru-runner.d.ts.map +1 -0
- package/dist/api-test/bru-runner.js +316 -0
- package/dist/api-test/bru-runner.js.map +1 -0
- package/dist/api-test/bru-writer.d.ts +52 -0
- package/dist/api-test/bru-writer.d.ts.map +1 -0
- package/dist/api-test/bru-writer.js +159 -0
- package/dist/api-test/bru-writer.js.map +1 -0
- package/dist/api-test/call-chain-extractor.d.ts +80 -0
- package/dist/api-test/call-chain-extractor.d.ts.map +1 -0
- package/dist/api-test/call-chain-extractor.js +179 -0
- package/dist/api-test/call-chain-extractor.js.map +1 -0
- package/dist/api-test/cli.d.ts +133 -0
- package/dist/api-test/cli.d.ts.map +1 -0
- package/dist/api-test/cli.js +1009 -0
- package/dist/api-test/cli.js.map +1 -0
- package/dist/api-test/config.d.ts +75 -0
- package/dist/api-test/config.d.ts.map +1 -0
- package/dist/api-test/config.js +406 -0
- package/dist/api-test/config.js.map +1 -0
- package/dist/api-test/db-query-cli.d.ts +51 -0
- package/dist/api-test/db-query-cli.d.ts.map +1 -0
- package/dist/api-test/db-query-cli.js +119 -0
- package/dist/api-test/db-query-cli.js.map +1 -0
- package/dist/api-test/enhance-prepare.d.ts +111 -0
- package/dist/api-test/enhance-prepare.d.ts.map +1 -0
- package/dist/api-test/enhance-prepare.js +425 -0
- package/dist/api-test/enhance-prepare.js.map +1 -0
- package/dist/api-test/enhance-write.d.ts +28 -0
- package/dist/api-test/enhance-write.d.ts.map +1 -0
- package/dist/api-test/enhance-write.js +145 -0
- package/dist/api-test/enhance-write.js.map +1 -0
- package/dist/api-test/errors.d.ts +48 -0
- package/dist/api-test/errors.d.ts.map +1 -0
- package/dist/api-test/errors.js +76 -0
- package/dist/api-test/errors.js.map +1 -0
- package/dist/api-test/field-extractor.d.ts +98 -0
- package/dist/api-test/field-extractor.d.ts.map +1 -0
- package/dist/api-test/field-extractor.js +327 -0
- package/dist/api-test/field-extractor.js.map +1 -0
- package/dist/api-test/impl-finder.d.ts +37 -0
- package/dist/api-test/impl-finder.d.ts.map +1 -0
- package/dist/api-test/impl-finder.js +54 -0
- package/dist/api-test/impl-finder.js.map +1 -0
- package/dist/api-test/index.d.ts +41 -0
- package/dist/api-test/index.d.ts.map +1 -0
- package/dist/api-test/index.js +124 -0
- package/dist/api-test/index.js.map +1 -0
- package/dist/api-test/java-parser.d.ts +89 -0
- package/dist/api-test/java-parser.d.ts.map +1 -0
- package/dist/api-test/java-parser.js +508 -0
- package/dist/api-test/java-parser.js.map +1 -0
- package/dist/api-test/md-writer.d.ts +49 -0
- package/dist/api-test/md-writer.d.ts.map +1 -0
- package/dist/api-test/md-writer.js +202 -0
- package/dist/api-test/md-writer.js.map +1 -0
- package/dist/api-test/parser-httpservice.d.ts +91 -0
- package/dist/api-test/parser-httpservice.d.ts.map +1 -0
- package/dist/api-test/parser-httpservice.js +271 -0
- package/dist/api-test/parser-httpservice.js.map +1 -0
- package/dist/api-test/report.d.ts +188 -0
- package/dist/api-test/report.d.ts.map +1 -0
- package/dist/api-test/report.js +522 -0
- package/dist/api-test/report.js.map +1 -0
- package/dist/api-test/snapshot.d.ts +26 -0
- package/dist/api-test/snapshot.d.ts.map +1 -0
- package/dist/api-test/snapshot.js +150 -0
- package/dist/api-test/snapshot.js.map +1 -0
- package/dist/api-test/test-history.d.ts +48 -0
- package/dist/api-test/test-history.d.ts.map +1 -0
- package/dist/api-test/test-history.js +122 -0
- package/dist/api-test/test-history.js.map +1 -0
- package/dist/api-test/types.d.ts +174 -0
- package/dist/api-test/types.d.ts.map +1 -0
- package/dist/api-test/types.js +13 -0
- package/dist/api-test/types.js.map +1 -0
- package/dist/api-test/verify-prepare.d.ts +30 -0
- package/dist/api-test/verify-prepare.d.ts.map +1 -0
- package/dist/api-test/verify-prepare.js +150 -0
- package/dist/api-test/verify-prepare.js.map +1 -0
- package/dist/api-test/verify-write.d.ts +31 -0
- package/dist/api-test/verify-write.d.ts.map +1 -0
- package/dist/api-test/verify-write.js +159 -0
- package/dist/api-test/verify-write.js.map +1 -0
- package/dist/dbquery/dump-schema.d.ts +46 -0
- package/dist/dbquery/dump-schema.d.ts.map +1 -0
- package/dist/dbquery/dump-schema.js +379 -0
- package/dist/dbquery/dump-schema.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +15 -0
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +53 -0
- package/dist/installer/targets/claude.js.map +1 -1
- package/package.json +1 -1
- package/scripts/agent-eval/block-cgraphx-and-gitnexus-cli-hook.sh +0 -43
- package/scripts/agent-eval/block-cgraphx-cli-hook.sh +0 -32
- package/scripts/agent-eval/block-cgraphx-cli-settings.json +0 -16
- package/scripts/agent-eval/cli-vs-mcp-3arm.sh +0 -121
- package/scripts/agent-eval/multi-tool-eval.sh +0 -171
- package/scripts/agent-eval/parse-cli-vs-mcp.mjs +0 -232
- package/scripts/agent-eval/parse-multi-tool.mjs +0 -242
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI wiring for the api-test subsystem.
|
|
4
|
+
*
|
|
5
|
+
* Exposes `registerApiTestCommands(program)` — called once from
|
|
6
|
+
* `src/bin/codegraph.ts`. All api-test subcommands live under
|
|
7
|
+
* `codegraph api-test <sub>`. The scan subcommand (T-20) is wired here;
|
|
8
|
+
* the analyze/assemble/run/report/list subcommands will be added in
|
|
9
|
+
* T-30 / T-40 / T-50 / T-60.
|
|
10
|
+
*
|
|
11
|
+
* Spec contract — scan:
|
|
12
|
+
* - Full re-scan (delete `docs/api-test/<service>/` subtree, rebuild).
|
|
13
|
+
* - Writes one `.bru` + one `.md` per endpoint under
|
|
14
|
+
* `docs/api-test/<service>/<svc_id>_<methodName>.{bru,md}`.
|
|
15
|
+
* - Writes `docs/api-test/bruno.json` at the api-test root with the
|
|
16
|
+
* minimal Bruno collection schema.
|
|
17
|
+
* - Failed endpoints don't abort the rest — warnings go to stderr.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 success (or partial success with warnings)
|
|
21
|
+
* 2 config error (ConfigError — missing/invalid profile or config)
|
|
22
|
+
* 5 argument error
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.__INTERNAL_CLI = void 0;
|
|
59
|
+
exports.registerApiTestCommands = registerApiTestCommands;
|
|
60
|
+
const fs = __importStar(require("node:fs"));
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
const config_1 = require("./config");
|
|
63
|
+
const errors_1 = require("./errors");
|
|
64
|
+
const parser_httpservice_1 = require("./parser-httpservice");
|
|
65
|
+
const bru_writer_1 = require("./bru-writer");
|
|
66
|
+
const md_writer_1 = require("./md-writer");
|
|
67
|
+
const java_parser_1 = require("./java-parser");
|
|
68
|
+
const impl_finder_1 = require("./impl-finder");
|
|
69
|
+
const field_extractor_1 = require("./field-extractor");
|
|
70
|
+
const call_chain_extractor_1 = require("./call-chain-extractor");
|
|
71
|
+
const assemble_1 = require("./assemble");
|
|
72
|
+
const bru_runner_1 = require("./bru-runner");
|
|
73
|
+
const test_history_1 = require("./test-history");
|
|
74
|
+
const report_1 = require("./report");
|
|
75
|
+
const errors_2 = require("./errors");
|
|
76
|
+
const enhance_prepare_1 = require("./enhance-prepare");
|
|
77
|
+
const enhance_write_1 = require("./enhance-write");
|
|
78
|
+
const snapshot_1 = require("./snapshot");
|
|
79
|
+
const verify_prepare_1 = require("./verify-prepare");
|
|
80
|
+
const verify_write_1 = require("./verify-write");
|
|
81
|
+
/** Default output root (relative to project root). */
|
|
82
|
+
const DEFAULT_OUTPUT_DIR = 'docs/api-test';
|
|
83
|
+
/** Minimal Bruno collection config — written to `<outputDir>/bruno.json`. */
|
|
84
|
+
const BRUNO_COLLECTION_CONFIG = {
|
|
85
|
+
version: 1,
|
|
86
|
+
name: 'hx-api-test',
|
|
87
|
+
type: 'collection',
|
|
88
|
+
};
|
|
89
|
+
class ArgError extends Error {
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Attach the `api-test` command group to a commander program.
|
|
93
|
+
*/
|
|
94
|
+
function registerApiTestCommands(program) {
|
|
95
|
+
const apiTest = program
|
|
96
|
+
.command('api-test')
|
|
97
|
+
.description('Java HTTP-service scan + Bruno test scaffolding (independent subsystem)');
|
|
98
|
+
// -- api-test scan --------------------------------------------------------
|
|
99
|
+
apiTest
|
|
100
|
+
.command('scan')
|
|
101
|
+
.description('Scan Java source for @Center interfaces and regenerate .bru + .md pairs under docs/api-test/')
|
|
102
|
+
.option('--project-root <path>', 'Project root to scan (defaults to cwd)', process.cwd())
|
|
103
|
+
.option('--output-dir <path>', 'Output directory (defaults to <projectRoot>/docs/api-test)')
|
|
104
|
+
.option('--filter <pattern>', 'Only emit endpoints whose service name matches the regex (optional)')
|
|
105
|
+
.option('--dry-run', 'Resolve + render, but do not write files')
|
|
106
|
+
.action(async (options) => {
|
|
107
|
+
try {
|
|
108
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
109
|
+
const outputDir = path.resolve(options.outputDir ?? path.join(projectRoot, DEFAULT_OUTPUT_DIR));
|
|
110
|
+
const config = (0, config_1.loadConfig)(projectRoot);
|
|
111
|
+
// Filter regex (optional)
|
|
112
|
+
let filterRe = null;
|
|
113
|
+
if (options.filter) {
|
|
114
|
+
try {
|
|
115
|
+
filterRe = new RegExp(options.filter);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new ArgError(`--filter is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Full re-scan: clear the existing output subtree before rebuilding.
|
|
122
|
+
// bruno.json is also rewritten below, so we don't preserve it.
|
|
123
|
+
if (!options.dryRun) {
|
|
124
|
+
await clearOutputDir(outputDir);
|
|
125
|
+
}
|
|
126
|
+
const endpoints = await (0, parser_httpservice_1.scanEndpoints)(projectRoot, config);
|
|
127
|
+
const filtered = filterRe
|
|
128
|
+
? endpoints.filter((e) => filterRe.test(e.service))
|
|
129
|
+
: endpoints;
|
|
130
|
+
let okCount = 0;
|
|
131
|
+
let warnCount = 0;
|
|
132
|
+
for (const endpoint of filtered) {
|
|
133
|
+
try {
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
// Dry-run: render to stdout (or just count) — don't write files.
|
|
136
|
+
const bru = (0, bru_writer_1.renderBru)(endpoint, config);
|
|
137
|
+
const md = (0, md_writer_1.renderMd)(endpoint);
|
|
138
|
+
void bru;
|
|
139
|
+
void md;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
await (0, bru_writer_1.writeBru)(endpoint, config, outputDir);
|
|
143
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
144
|
+
}
|
|
145
|
+
okCount++;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
warnCount++;
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
process.stderr.write(`[api-test] warning: failed to write endpoint ${endpoint.svcId}_${endpoint.method}: ${msg}\n`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Write bruno.json at the api-test root (collection metadata).
|
|
154
|
+
if (!options.dryRun) {
|
|
155
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
156
|
+
const brunoJsonPath = path.join(outputDir, 'bruno.json');
|
|
157
|
+
await fs.promises.writeFile(brunoJsonPath, JSON.stringify(BRUNO_COLLECTION_CONFIG, null, 2) + '\n', 'utf8');
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write(`[api-test] scan complete: ${okCount} endpoint(s) written` +
|
|
160
|
+
(warnCount > 0 ? `, ${warnCount} warning(s)` : '') +
|
|
161
|
+
(options.dryRun ? ' (dry-run)' : '') +
|
|
162
|
+
` → ${outputDir}\n`);
|
|
163
|
+
if (warnCount > 0)
|
|
164
|
+
process.exitCode = 0; // partial success still 0
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
renderErrorAndExit(err);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// -- api-test analyze -----------------------------------------------------
|
|
171
|
+
apiTest
|
|
172
|
+
.command('analyze')
|
|
173
|
+
.description('Analyze Java impl classes to fill fields / call_chain / dep_tables / auth_required in .md files')
|
|
174
|
+
.option('--project-root <path>', 'Project root to scan (defaults to cwd)', process.cwd())
|
|
175
|
+
.option('--output-dir <path>', 'Output directory (defaults to <projectRoot>/docs/api-test)')
|
|
176
|
+
.option('--filter <pattern>', 'Only analyze endpoints whose service name matches the regex (optional)')
|
|
177
|
+
.option('--dry-run', 'Resolve + extract, but do not write files')
|
|
178
|
+
.action(async (options) => {
|
|
179
|
+
try {
|
|
180
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
181
|
+
const outputDir = path.resolve(options.outputDir ?? path.join(projectRoot, DEFAULT_OUTPUT_DIR));
|
|
182
|
+
const config = (0, config_1.loadConfig)(projectRoot);
|
|
183
|
+
// Per spec §9, analyze does NOT load the profile (only run does).
|
|
184
|
+
// Profile.json absence must not fail the analyze phase.
|
|
185
|
+
// Filter regex (optional)
|
|
186
|
+
let filterRe = null;
|
|
187
|
+
if (options.filter) {
|
|
188
|
+
try {
|
|
189
|
+
filterRe = new RegExp(options.filter);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
throw new ArgError(`--filter is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Re-scan to get the fresh Endpoint[] (full overwrite — we don't
|
|
196
|
+
// read existing .md files because re-scan gives us the same metadata
|
|
197
|
+
// plus a clean slate; "全部覆盖" semantics).
|
|
198
|
+
const endpoints = await (0, parser_httpservice_1.scanEndpoints)(projectRoot, config);
|
|
199
|
+
const filtered = filterRe
|
|
200
|
+
? endpoints.filter((e) => filterRe.test(e.service))
|
|
201
|
+
: endpoints;
|
|
202
|
+
// Build the allClasses cache: parse every .java file under
|
|
203
|
+
// projectRoot once, accumulate ClassInfo[]. Used by findImplFor,
|
|
204
|
+
// findVoClass (called by extractFields).
|
|
205
|
+
const allClasses = [];
|
|
206
|
+
for (const javaFile of iterJavaFilesCached(projectRoot)) {
|
|
207
|
+
try {
|
|
208
|
+
const classes = await (0, java_parser_1.parseJavaFile)(javaFile);
|
|
209
|
+
allClasses.push(...classes);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
213
|
+
process.stderr.write(`[api-test] warning: failed to parse ${javaFile}: ${msg}\n`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let okCount = 0;
|
|
217
|
+
let skipCount = 0;
|
|
218
|
+
let warnCount = 0;
|
|
219
|
+
for (const endpoint of filtered) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await analyzeEndpoint(endpoint, allClasses);
|
|
222
|
+
endpoint.fields = result.fields;
|
|
223
|
+
endpoint.callChain = result.callChain;
|
|
224
|
+
endpoint.depTables = result.depTables;
|
|
225
|
+
endpoint.authRequired = result.authRequired;
|
|
226
|
+
if (result.skipReason) {
|
|
227
|
+
endpoint.status = 'skip';
|
|
228
|
+
endpoint.skipReason = result.skipReason;
|
|
229
|
+
skipCount++;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
endpoint.status = 'active';
|
|
233
|
+
endpoint.skipReason = null;
|
|
234
|
+
okCount++;
|
|
235
|
+
}
|
|
236
|
+
if (!options.dryRun) {
|
|
237
|
+
// Re-render + overwrite the .md file (full overwrite).
|
|
238
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
warnCount++;
|
|
243
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
244
|
+
process.stderr.write(`[api-test] warning: failed to analyze endpoint ${endpoint.svcId}_${endpoint.method}: ${msg}\n`);
|
|
245
|
+
// Mark as skip so the failure is visible in the .md.
|
|
246
|
+
endpoint.status = 'skip';
|
|
247
|
+
endpoint.skipReason = `analyze_error: ${msg}`;
|
|
248
|
+
if (!options.dryRun) {
|
|
249
|
+
try {
|
|
250
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// swallow — best-effort
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
process.stdout.write(`[api-test] analyze complete: ${okCount} ok, ${skipCount} skipped` +
|
|
259
|
+
(warnCount > 0 ? `, ${warnCount} warning(s)` : '') +
|
|
260
|
+
(options.dryRun ? ' (dry-run)' : '') +
|
|
261
|
+
` → ${outputDir}\n`);
|
|
262
|
+
if (warnCount > 0)
|
|
263
|
+
process.exitCode = 0;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
renderErrorAndExit(err);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// -- api-test assemble -----------------------------------------------------
|
|
270
|
+
apiTest
|
|
271
|
+
.command('assemble')
|
|
272
|
+
.description('Assemble request bodies from fixture/db-query data and write them into .bru body:json blocks')
|
|
273
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
274
|
+
.option('--output-dir <path>', 'Output directory (defaults to <projectRoot>/docs/api-test)')
|
|
275
|
+
.option('--filter <pattern>', 'Only assemble endpoints whose service name matches the regex (optional)')
|
|
276
|
+
.option('--dry-run', 'Resolve + assemble, but do not write files')
|
|
277
|
+
.action(async (options) => {
|
|
278
|
+
try {
|
|
279
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
280
|
+
const outputDir = path.resolve(options.outputDir ?? path.join(projectRoot, DEFAULT_OUTPUT_DIR));
|
|
281
|
+
const config = (0, config_1.loadConfig)(projectRoot);
|
|
282
|
+
const fixtures = (0, config_1.loadFixtures)(projectRoot);
|
|
283
|
+
// Per spec §9, assemble does NOT load the profile (only run does).
|
|
284
|
+
// The profile is not needed for body assembly — Bruno's `{{var}}`
|
|
285
|
+
// syntax resolves env variables at request time during the run phase.
|
|
286
|
+
// Filter regex (optional)
|
|
287
|
+
let filterRe = null;
|
|
288
|
+
if (options.filter) {
|
|
289
|
+
try {
|
|
290
|
+
filterRe = new RegExp(options.filter);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
throw new ArgError(`--filter is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Re-scan + re-analyze to get fresh endpoints with fields populated.
|
|
297
|
+
// (Same pattern as the analyze CLI — "full overwrite" semantics. The
|
|
298
|
+
// .md files are human-readable artifacts; the assemble phase doesn't
|
|
299
|
+
// parse them back, it re-derives from source so the body always
|
|
300
|
+
// matches the current code.)
|
|
301
|
+
const endpoints = await (0, parser_httpservice_1.scanEndpoints)(projectRoot, config);
|
|
302
|
+
const filtered = filterRe
|
|
303
|
+
? endpoints.filter((e) => filterRe.test(e.service))
|
|
304
|
+
: endpoints;
|
|
305
|
+
// Build the allClasses cache for analyze.
|
|
306
|
+
const allClasses = [];
|
|
307
|
+
for (const javaFile of iterJavaFilesCached(projectRoot)) {
|
|
308
|
+
try {
|
|
309
|
+
const classes = await (0, java_parser_1.parseJavaFile)(javaFile);
|
|
310
|
+
allClasses.push(...classes);
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
314
|
+
process.stderr.write(`[api-test] warning: failed to parse ${javaFile}: ${msg}\n`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
let okCount = 0;
|
|
318
|
+
let skipCount = 0;
|
|
319
|
+
let warnCount = 0;
|
|
320
|
+
for (const endpoint of filtered) {
|
|
321
|
+
try {
|
|
322
|
+
// Re-analyze to populate fields/callChain/depTables/authRequired.
|
|
323
|
+
const result = await analyzeEndpoint(endpoint, allClasses);
|
|
324
|
+
endpoint.fields = result.fields;
|
|
325
|
+
endpoint.callChain = result.callChain;
|
|
326
|
+
endpoint.depTables = result.depTables;
|
|
327
|
+
endpoint.authRequired = result.authRequired;
|
|
328
|
+
if (result.skipReason) {
|
|
329
|
+
// Analyze-phase skip (impl_not_found / param_no_strong_type) —
|
|
330
|
+
// propagate to .md, don't attempt assemble.
|
|
331
|
+
endpoint.status = 'skip';
|
|
332
|
+
endpoint.skipReason = result.skipReason;
|
|
333
|
+
skipCount++;
|
|
334
|
+
if (!options.dryRun) {
|
|
335
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
endpoint.status = 'active';
|
|
340
|
+
endpoint.skipReason = null;
|
|
341
|
+
// Only assemble `active` endpoints.
|
|
342
|
+
const assembled = await (0, assemble_1.assembleEndpoint)(endpoint, config, fixtures, projectRoot);
|
|
343
|
+
if (assembled.skipped) {
|
|
344
|
+
endpoint.status = 'skip';
|
|
345
|
+
endpoint.skipReason = assembled.reason;
|
|
346
|
+
skipCount++;
|
|
347
|
+
if (!options.dryRun) {
|
|
348
|
+
// Update .md with the skip status. Don't touch .bru body for
|
|
349
|
+
// skipped endpoints (per spec: "skip 状态的 endpoint → 不修改 .bru body").
|
|
350
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
okCount++;
|
|
355
|
+
if (!options.dryRun) {
|
|
356
|
+
// Write the assembled body into the .bru's body:json block.
|
|
357
|
+
// First ensure the .bru exists (scan phase writes it; if the
|
|
358
|
+
// user skipped scan, we write it fresh here).
|
|
359
|
+
if (!endpoint.bruPath || !fs.existsSync(endpoint.bruPath)) {
|
|
360
|
+
await (0, bru_writer_1.writeBru)(endpoint, config, outputDir);
|
|
361
|
+
}
|
|
362
|
+
await (0, assemble_1.writeBruBody)(endpoint.bruPath, assembled.body);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
warnCount++;
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
369
|
+
process.stderr.write(`[api-test] warning: failed to assemble endpoint ${endpoint.svcId}_${endpoint.method}: ${msg}\n`);
|
|
370
|
+
endpoint.status = 'skip';
|
|
371
|
+
endpoint.skipReason = `assemble_error: ${msg}`;
|
|
372
|
+
if (!options.dryRun) {
|
|
373
|
+
try {
|
|
374
|
+
await (0, md_writer_1.writeMd)(endpoint, outputDir);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// swallow — best-effort
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
process.stdout.write(`[api-test] assemble complete: ${okCount} ok, ${skipCount} skipped` +
|
|
383
|
+
(warnCount > 0 ? `, ${warnCount} warning(s)` : '') +
|
|
384
|
+
(options.dryRun ? ' (dry-run)' : '') +
|
|
385
|
+
` → ${outputDir}\n`);
|
|
386
|
+
if (warnCount > 0)
|
|
387
|
+
process.exitCode = 0;
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
renderErrorAndExit(err);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// -- api-test run ----------------------------------------------------------
|
|
394
|
+
apiTest
|
|
395
|
+
.command('run')
|
|
396
|
+
.description('Run assembled .bru requests via Bruno CLI and append results to JSONL history')
|
|
397
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
398
|
+
.option('--output-dir <path>', 'Collection directory (defaults to <projectRoot>/docs/api-test)')
|
|
399
|
+
.option('--filter <pattern>', 'Only run endpoints whose service name matches the regex (optional)')
|
|
400
|
+
.option('--status <status>', 'Only run endpoints with this status (active|skip)', 'active')
|
|
401
|
+
.action(async (options) => {
|
|
402
|
+
try {
|
|
403
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
404
|
+
const outputDir = path.resolve(options.outputDir ?? path.join(projectRoot, DEFAULT_OUTPUT_DIR));
|
|
405
|
+
const config = (0, config_1.loadConfig)(projectRoot);
|
|
406
|
+
const profile = (0, config_1.loadProfile)(projectRoot);
|
|
407
|
+
// Validate --status.
|
|
408
|
+
const statusFilter = options.status ?? 'active';
|
|
409
|
+
if (statusFilter !== 'active' && statusFilter !== 'skip') {
|
|
410
|
+
throw new ArgError(`--status must be "active" or "skip", got: ${statusFilter}`);
|
|
411
|
+
}
|
|
412
|
+
// Filter regex (optional) — narrows by service name.
|
|
413
|
+
let filterRe = null;
|
|
414
|
+
if (options.filter) {
|
|
415
|
+
try {
|
|
416
|
+
filterRe = new RegExp(options.filter);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
throw new ArgError(`--filter is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Re-scan to get the endpoint list (service + method + bruPath +
|
|
423
|
+
// mdPath). The .bru files are assumed to already exist on disk from
|
|
424
|
+
// a prior scan/analyze/assemble — run doesn't write .bru files.
|
|
425
|
+
const endpoints = await (0, parser_httpservice_1.scanEndpoints)(projectRoot, config);
|
|
426
|
+
const filteredEndpoints = filterRe
|
|
427
|
+
? endpoints.filter((e) => filterRe.test(e.service))
|
|
428
|
+
: endpoints;
|
|
429
|
+
// Read each endpoint's .md file to determine status. Endpoints whose
|
|
430
|
+
// .md doesn't exist yet (scan never ran) default to 'active' so a
|
|
431
|
+
// fresh collection still runs.
|
|
432
|
+
const endpointStatuses = new Map();
|
|
433
|
+
for (const e of filteredEndpoints) {
|
|
434
|
+
endpointStatuses.set(`${e.svcId}_${e.method}`, readEndpointStatus(e, outputDir));
|
|
435
|
+
}
|
|
436
|
+
// Apply --status filter: only keep endpoints whose status matches.
|
|
437
|
+
// Bruno CLI runs the whole collection dir (or a service subdir when
|
|
438
|
+
// --filter narrows to one service); we filter the RESULTS by status
|
|
439
|
+
// after the run.
|
|
440
|
+
const filterService = pickFilterService(filterRe, filteredEndpoints);
|
|
441
|
+
let results;
|
|
442
|
+
try {
|
|
443
|
+
results = await (0, bru_runner_1.runBrunoCollection)(outputDir, profile, filterService ? { service: filterService } : undefined);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
if (err instanceof errors_1.BrunoCliError) {
|
|
447
|
+
process.stderr.write(`[api-test] ${err.message}\n`);
|
|
448
|
+
process.exitCode = 4;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
453
|
+
// Match results back to endpoints by name to enrich with service +
|
|
454
|
+
// status info. Results without a matching endpoint are still recorded
|
|
455
|
+
// (service = '(unknown)').
|
|
456
|
+
const endpointByName = new Map();
|
|
457
|
+
for (const e of filteredEndpoints) {
|
|
458
|
+
endpointByName.set(`${e.svcId}_${e.method}`, e);
|
|
459
|
+
}
|
|
460
|
+
const ranAt = new Date().toISOString();
|
|
461
|
+
let passedCount = 0;
|
|
462
|
+
let failedCount = 0;
|
|
463
|
+
let skippedCount = 0;
|
|
464
|
+
for (const result of results) {
|
|
465
|
+
const endpoint = endpointByName.get(result.endpoint);
|
|
466
|
+
const service = endpoint?.service ?? '(unknown)';
|
|
467
|
+
const status = endpointStatuses.get(result.endpoint) ?? 'active';
|
|
468
|
+
// --status filter: skip endpoints whose status doesn't match.
|
|
469
|
+
if (status !== statusFilter) {
|
|
470
|
+
skippedCount++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const judgment = judgePass(result, profile);
|
|
474
|
+
const testRun = {
|
|
475
|
+
endpoint: result.endpoint,
|
|
476
|
+
service,
|
|
477
|
+
ranAt,
|
|
478
|
+
httpStatus: result.httpStatus,
|
|
479
|
+
bizCode: judgment.bizCode,
|
|
480
|
+
durationMs: result.durationMs,
|
|
481
|
+
passed: judgment.passed,
|
|
482
|
+
error: judgment.error,
|
|
483
|
+
};
|
|
484
|
+
try {
|
|
485
|
+
(0, test_history_1.appendTestRunSync)(testRun, projectRoot);
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
489
|
+
process.stderr.write(`[api-test] warning: failed to append history for ${result.endpoint}: ${msg}\n`);
|
|
490
|
+
}
|
|
491
|
+
if (judgment.passed) {
|
|
492
|
+
passedCount++;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
failedCount++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
process.stdout.write(`[api-test] run complete: ${passedCount} passed, ${failedCount} failed` +
|
|
499
|
+
(skippedCount > 0 ? `, ${skippedCount} skipped` : '') +
|
|
500
|
+
` → ${outputDir}\n`);
|
|
501
|
+
// Non-zero exit when any endpoint failed, so CI can gate on it.
|
|
502
|
+
if (failedCount > 0)
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
renderErrorAndExit(err);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
// -- api-test report -------------------------------------------------------
|
|
510
|
+
apiTest
|
|
511
|
+
.command('report')
|
|
512
|
+
.description('Aggregate the day\'s JSONL run history + .md catalog into a Markdown report')
|
|
513
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
514
|
+
.option('--date <YYYY-MM-DD>', 'Report date, defaults to today (local)')
|
|
515
|
+
.action(async (options) => {
|
|
516
|
+
try {
|
|
517
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
518
|
+
const date = options.date ?? todayLocalYmd();
|
|
519
|
+
const md = await (0, report_1.generateReport)(projectRoot, date);
|
|
520
|
+
const outPath = (0, report_1.reportFilePath)(projectRoot, date);
|
|
521
|
+
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
|
|
522
|
+
await fs.promises.writeFile(outPath, md + '\n', 'utf8');
|
|
523
|
+
process.stdout.write(`${outPath}\n`);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
renderErrorAndExit(err);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// -- api-test list ---------------------------------------------------------
|
|
530
|
+
apiTest
|
|
531
|
+
.command('list')
|
|
532
|
+
.description('List every catalog endpoint with its most recent run result, after filters')
|
|
533
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
534
|
+
.option('--service <name>', 'Filter by service name (exact match)')
|
|
535
|
+
.option('--status <status>', 'Filter by status (active|skip)')
|
|
536
|
+
.option('--svc-id <id>', 'Filter by svc_id (exact match)')
|
|
537
|
+
.action(async (options) => {
|
|
538
|
+
try {
|
|
539
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
540
|
+
const filters = {};
|
|
541
|
+
if (options.service)
|
|
542
|
+
filters.service = options.service;
|
|
543
|
+
if (options.svcId)
|
|
544
|
+
filters.svcId = options.svcId;
|
|
545
|
+
if (options.status) {
|
|
546
|
+
if (options.status !== 'active' && options.status !== 'skip') {
|
|
547
|
+
throw new ArgError(`--status must be "active" or "skip", got: ${options.status}`);
|
|
548
|
+
}
|
|
549
|
+
filters.status = options.status;
|
|
550
|
+
}
|
|
551
|
+
const rows = (0, report_1.listEndpoints)(projectRoot, filters);
|
|
552
|
+
if (rows.length === 0) {
|
|
553
|
+
process.stdout.write('(no endpoints match the filters)\n');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const table = (0, report_1.renderListTable)(rows);
|
|
557
|
+
process.stdout.write(table + '\n');
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
renderErrorAndExit(err);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// -- api-test enhance-prepare ---------------------------------------------
|
|
564
|
+
apiTest
|
|
565
|
+
.command('enhance-prepare')
|
|
566
|
+
.description('Aggregate catalog .md + impl path + db sample rows + prior AI fields into a JSON payload for AI field inference')
|
|
567
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
568
|
+
.option('--service <name>', 'Service subdirectory under docs/api-test/ (defaults to inst-service)', 'inst-service')
|
|
569
|
+
.option('--endpoint <name>', 'Endpoint stem <svc_id>_<method> (optional; defaults to all .md in the service dir)')
|
|
570
|
+
.action(async (options) => {
|
|
571
|
+
try {
|
|
572
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
573
|
+
const service = options.service ?? 'inst-service';
|
|
574
|
+
const results = await (0, enhance_prepare_1.enhancePrepare)(projectRoot, service, options.endpoint);
|
|
575
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
renderErrorAndExit(err);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
// -- api-test enhance-write -----------------------------------------------
|
|
582
|
+
apiTest
|
|
583
|
+
.command('enhance-write')
|
|
584
|
+
.description('Re-generate a .bru file with AI-produced docs/tests/body blocks and persist AI-inferred fields')
|
|
585
|
+
.requiredOption('--endpoint <name>', 'Endpoint stem <svc_id>_<method>')
|
|
586
|
+
.requiredOption('--docs <file>', 'AI-generated docs block content (markdown)')
|
|
587
|
+
.requiredOption('--tests <file>', 'AI-generated tests block content (JS)')
|
|
588
|
+
.requiredOption('--body <file>', 'AI-assembled body JSON')
|
|
589
|
+
.option('--ai-fields <file>', 'AI-inferred source_table fields JSON (optional)')
|
|
590
|
+
.option('--service <name>', 'Service subdirectory (defaults to inst-service)', 'inst-service')
|
|
591
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
592
|
+
.action(async (options) => {
|
|
593
|
+
try {
|
|
594
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
595
|
+
const bruPath = await (0, enhance_write_1.enhanceWrite)({
|
|
596
|
+
projectRoot,
|
|
597
|
+
endpoint: options.endpoint,
|
|
598
|
+
docsFile: options.docs,
|
|
599
|
+
testsFile: options.tests,
|
|
600
|
+
bodyFile: options.body,
|
|
601
|
+
aiFieldsFile: options.aiFields ?? null,
|
|
602
|
+
service: options.service ?? 'inst-service',
|
|
603
|
+
});
|
|
604
|
+
process.stdout.write(bruPath + '\n');
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
renderErrorAndExit(err);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
// -- api-test snapshot -----------------------------------------------------
|
|
611
|
+
apiTest
|
|
612
|
+
.command('snapshot')
|
|
613
|
+
.description('Capture dep_tables row snapshots before bru run, stored as runs/<run-id>-before.json')
|
|
614
|
+
.requiredOption('--endpoint <name>', 'Endpoint stem <svc_id>_<method>')
|
|
615
|
+
.requiredOption('--run-id <id>', 'Run identifier for before.json filename')
|
|
616
|
+
.option('--service <name>', 'Service subdirectory (defaults to inst-service)', 'inst-service')
|
|
617
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
618
|
+
.action(async (options) => {
|
|
619
|
+
try {
|
|
620
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
621
|
+
const result = await (0, snapshot_1.snapshot)(projectRoot, options.endpoint, options.runId, options.service ?? 'inst-service');
|
|
622
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
renderErrorAndExit(err);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
// -- api-test verify-prepare ----------------------------------------------
|
|
629
|
+
apiTest
|
|
630
|
+
.command('verify-prepare')
|
|
631
|
+
.description('Read bru run report + before.json + post-run db snapshot, output structured JSON for AI verify')
|
|
632
|
+
.requiredOption('--endpoint <name>', 'Endpoint stem <svc_id>_<method>')
|
|
633
|
+
.requiredOption('--results <file>', 'bru run --output JSON report path')
|
|
634
|
+
.requiredOption('--run-id <id>', 'Run identifier for before.json lookup')
|
|
635
|
+
.option('--service <name>', 'Service subdirectory (defaults to inst-service)', 'inst-service')
|
|
636
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
637
|
+
.action(async (options) => {
|
|
638
|
+
try {
|
|
639
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
640
|
+
const result = await (0, verify_prepare_1.verifyPrepare)(projectRoot, options.endpoint, options.results, options.runId, options.service ?? 'inst-service');
|
|
641
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
renderErrorAndExit(err);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
// -- api-test verify-write ------------------------------------------------
|
|
648
|
+
apiTest
|
|
649
|
+
.command('verify-write')
|
|
650
|
+
.description('Read verify-prepare JSON + AI temp files, compute overall_passed, append to ai JSONL')
|
|
651
|
+
.requiredOption('--endpoint <name>', 'Endpoint stem <svc_id>_<method>')
|
|
652
|
+
.requiredOption('--verify-prepare-json <file>', 'verify-prepare output JSON path')
|
|
653
|
+
.requiredOption('--ai-assertions <file>', 'AI-generated assertions result JSON')
|
|
654
|
+
.requiredOption('--db-verify <file>', 'AI-generated DB verify result JSON')
|
|
655
|
+
.requiredOption('--ran-at <iso>', 'ISO-8601 timestamp for JSONL filename + Layer 1 association')
|
|
656
|
+
.option('--service <name>', 'Service subdirectory (defaults to inst-service)', 'inst-service')
|
|
657
|
+
.option('--project-root <path>', 'Project root (defaults to cwd)', process.cwd())
|
|
658
|
+
.action(async (options) => {
|
|
659
|
+
try {
|
|
660
|
+
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
661
|
+
const jsonlPath = await (0, verify_write_1.verifyWrite)({
|
|
662
|
+
projectRoot,
|
|
663
|
+
endpoint: options.endpoint,
|
|
664
|
+
verifyPrepareJsonFile: options.verifyPrepareJson,
|
|
665
|
+
aiAssertionsFile: options.aiAssertions,
|
|
666
|
+
dbVerifyFile: options.dbVerify,
|
|
667
|
+
ranAt: options.ranAt,
|
|
668
|
+
service: options.service ?? 'inst-service',
|
|
669
|
+
});
|
|
670
|
+
process.stdout.write(jsonlPath + '\n');
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
renderErrorAndExit(err);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
678
|
+
/**
|
|
679
|
+
* Walk `projectRoot` and yield every `.java` file path. Same ignore list as
|
|
680
|
+
* the scan phase (parser-httpservice.iterJavaFiles) — duplicated here to
|
|
681
|
+
* avoid leaking the scan module's internal helper through the CLI surface.
|
|
682
|
+
* Symlinks are not followed.
|
|
683
|
+
*/
|
|
684
|
+
function* iterJavaFilesCached(root) {
|
|
685
|
+
let entries;
|
|
686
|
+
try {
|
|
687
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
if (entry.name.startsWith('.')) {
|
|
694
|
+
if (entry.isDirectory())
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (entry.isDirectory()) {
|
|
698
|
+
if (DEFAULT_IGNORE_DIRS.has(entry.name))
|
|
699
|
+
continue;
|
|
700
|
+
yield* iterJavaFilesCached(path.join(root, entry.name));
|
|
701
|
+
}
|
|
702
|
+
else if (entry.isFile() && entry.name.endsWith('.java')) {
|
|
703
|
+
yield path.join(root, entry.name);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** Default ignore list — mirrors parser-httpservice.DEFAULT_IGNORE_DIRS. */
|
|
708
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
709
|
+
'node_modules',
|
|
710
|
+
'dist',
|
|
711
|
+
'build',
|
|
712
|
+
'target',
|
|
713
|
+
'out',
|
|
714
|
+
'.git',
|
|
715
|
+
'.cgraphx',
|
|
716
|
+
]);
|
|
717
|
+
/**
|
|
718
|
+
* Run the analyze pipeline for one endpoint:
|
|
719
|
+
* 1. findImplFor(interfaceFqn, allClasses) — locate the impl class.
|
|
720
|
+
* 2. Match the interface method to the impl method (name + param count).
|
|
721
|
+
* 3. extractFields + extractCallChain.
|
|
722
|
+
*
|
|
723
|
+
* Returns the enriched fields/callChain/depTables/authRequired, plus a
|
|
724
|
+
* `skipReason` string when the endpoint should be marked `status: skip`.
|
|
725
|
+
*/
|
|
726
|
+
async function analyzeEndpoint(endpoint, allClasses) {
|
|
727
|
+
// 1. Find the impl class.
|
|
728
|
+
const implInfo = await (0, impl_finder_1.findImplFor)(endpoint.interfaceFqn, allClasses);
|
|
729
|
+
if (!implInfo) {
|
|
730
|
+
return {
|
|
731
|
+
fields: [],
|
|
732
|
+
callChain: [],
|
|
733
|
+
depTables: [],
|
|
734
|
+
authRequired: false,
|
|
735
|
+
skipReason: 'impl_not_found',
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
// 2. Match the interface method to the impl method by name + param count.
|
|
739
|
+
// The interface method's signature is on `endpoint.signature` (a string),
|
|
740
|
+
// but we have `endpoint.method` (name) and can count params from the
|
|
741
|
+
// signature. The impl method's parameters are on `implInfo.methods`.
|
|
742
|
+
const paramCount = countParamsInSignature(endpoint.signature);
|
|
743
|
+
const implMethod = findImplMethod(implInfo, endpoint.method, paramCount);
|
|
744
|
+
if (!implMethod) {
|
|
745
|
+
return {
|
|
746
|
+
fields: [],
|
|
747
|
+
callChain: [],
|
|
748
|
+
depTables: [],
|
|
749
|
+
authRequired: false,
|
|
750
|
+
skipReason: 'impl_method_not_found',
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
// 3. Extract fields + call chain.
|
|
754
|
+
const fields = await (0, field_extractor_1.extractFields)(endpoint, implInfo, implMethod, allClasses);
|
|
755
|
+
// When reverse-engineering fails, extractFields returns [] — mark skip.
|
|
756
|
+
const fieldExtractionFailed = implMethod.parameters.length > 0 && fields.length === 0;
|
|
757
|
+
const { callChain, depTables, authRequired } = await (0, call_chain_extractor_1.extractCallChain)(implInfo, implMethod);
|
|
758
|
+
return {
|
|
759
|
+
fields,
|
|
760
|
+
callChain,
|
|
761
|
+
depTables,
|
|
762
|
+
authRequired,
|
|
763
|
+
skipReason: fieldExtractionFailed ? 'param_no_strong_type' : null,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/** Count parameters in a signature string like `Object qryOrderList(Object reqVo)`. */
|
|
767
|
+
function countParamsInSignature(signature) {
|
|
768
|
+
const m = /\((.*)\)/.exec(signature);
|
|
769
|
+
if (!m)
|
|
770
|
+
return 0;
|
|
771
|
+
const inner = (m[1] ?? '').trim();
|
|
772
|
+
if (!inner)
|
|
773
|
+
return 0;
|
|
774
|
+
// Naive split on commas — doesn't handle generic commas in `Map<K,V>` etc.
|
|
775
|
+
// For param-count matching, we strip generic angle-bracket contents first.
|
|
776
|
+
const cleaned = inner.replace(/<[^>]*>/g, '');
|
|
777
|
+
return cleaned.split(',').length;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Find the impl method matching `name` + `paramCount`. Falls back to
|
|
781
|
+
* name-only match if paramCount is 0 (defensive — shouldn't happen for
|
|
782
|
+
* real @Center methods, which always have ≥1 param).
|
|
783
|
+
*/
|
|
784
|
+
function findImplMethod(implInfo, name, paramCount) {
|
|
785
|
+
const byName = implInfo.methods.filter((m) => m.name === name);
|
|
786
|
+
if (byName.length === 0)
|
|
787
|
+
return null;
|
|
788
|
+
if (paramCount === 0)
|
|
789
|
+
return byName[0] ?? null;
|
|
790
|
+
const byArity = byName.find((m) => m.parameters.length === paramCount);
|
|
791
|
+
return byArity ?? byName[0] ?? null;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Remove the existing output subtree so a re-scan doesn't leave stale
|
|
795
|
+
* `.bru` / `.md` files behind for endpoints that no longer exist. Safe to
|
|
796
|
+
* call on a non-existent dir.
|
|
797
|
+
*
|
|
798
|
+
* Preserves any files / subdirs the user might have added that are NOT
|
|
799
|
+
* `.bru` / `.md` pairs? Spec says "full re-scan" = delete the whole
|
|
800
|
+
* `docs/api-test/<service>/` subtree and rewrite bruno.json. We follow
|
|
801
|
+
* that literally: delete the entire output dir's *.bru / *.md / bruno.json,
|
|
802
|
+
* and remove now-empty service subdirs.
|
|
803
|
+
*/
|
|
804
|
+
async function clearOutputDir(outputDir) {
|
|
805
|
+
let entries;
|
|
806
|
+
try {
|
|
807
|
+
entries = await fs.promises.readdir(outputDir, { withFileTypes: true });
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
if (err.code === 'ENOENT')
|
|
811
|
+
return;
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
for (const entry of entries) {
|
|
815
|
+
const full = path.join(outputDir, entry.name);
|
|
816
|
+
if (entry.isDirectory()) {
|
|
817
|
+
// Recurse into service subdirs and clear .bru / .md files; remove the
|
|
818
|
+
// subdir if it ends up empty.
|
|
819
|
+
await clearServiceSubdir(full);
|
|
820
|
+
}
|
|
821
|
+
else if (entry.isFile() && (entry.name.endsWith('.bru') || entry.name.endsWith('.md') || entry.name === 'bruno.json')) {
|
|
822
|
+
await fs.promises.unlink(full);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/** Clear `.bru` / `.md` files inside a service subdirectory; rmdir if empty. */
|
|
827
|
+
async function clearServiceSubdir(serviceDir) {
|
|
828
|
+
let entries;
|
|
829
|
+
try {
|
|
830
|
+
entries = await fs.promises.readdir(serviceDir, { withFileTypes: true });
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
if (err.code === 'ENOENT')
|
|
834
|
+
return;
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
let remaining = 0;
|
|
838
|
+
for (const entry of entries) {
|
|
839
|
+
const full = path.join(serviceDir, entry.name);
|
|
840
|
+
if (entry.isDirectory()) {
|
|
841
|
+
remaining++; // don't touch nested dirs the user added
|
|
842
|
+
}
|
|
843
|
+
else if (entry.isFile() && (entry.name.endsWith('.bru') || entry.name.endsWith('.md'))) {
|
|
844
|
+
await fs.promises.unlink(full);
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
remaining++;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (remaining === 0) {
|
|
851
|
+
await fs.promises.rmdir(serviceDir).catch(() => { });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function renderErrorAndExit(err) {
|
|
855
|
+
if (err instanceof ArgError) {
|
|
856
|
+
process.stderr.write(`[api-test] ${err.message}\n`);
|
|
857
|
+
process.exitCode = 5;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (err instanceof errors_1.ConfigError) {
|
|
861
|
+
process.stderr.write(`[api-test] config error: ${err.message}\n`);
|
|
862
|
+
process.exitCode = 2;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (err instanceof errors_2.ReportError) {
|
|
866
|
+
process.stderr.write(`[api-test] report error: ${err.message}\n`);
|
|
867
|
+
process.exitCode = 2;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (err instanceof errors_1.BrunoCliError) {
|
|
871
|
+
process.stderr.write(`[api-test] ${err.message}\n`);
|
|
872
|
+
process.exitCode = 4;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
876
|
+
process.stderr.write(`[api-test] Unexpected error: ${msg}\n`);
|
|
877
|
+
process.exitCode = 1;
|
|
878
|
+
}
|
|
879
|
+
/** Today's date in the local timezone as `YYYY-MM-DD`. */
|
|
880
|
+
function todayLocalYmd() {
|
|
881
|
+
const d = new Date();
|
|
882
|
+
const y = d.getFullYear();
|
|
883
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
884
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
885
|
+
return `${y}-${m}-${day}`;
|
|
886
|
+
}
|
|
887
|
+
// ─── Run-phase helpers ─────────────────────────────────────────────────────
|
|
888
|
+
/**
|
|
889
|
+
* Read an endpoint's status from its `.md` frontmatter. Returns 'active'
|
|
890
|
+
* when the .md doesn't exist or the status line can't be found (defensive
|
|
891
|
+
* — a fresh collection with no analyze phase still runs).
|
|
892
|
+
*/
|
|
893
|
+
function readEndpointStatus(endpoint, outputDir) {
|
|
894
|
+
const mdPath = path.join(outputDir, endpoint.service, `${endpoint.svcId}_${endpoint.method}.md`);
|
|
895
|
+
let content;
|
|
896
|
+
try {
|
|
897
|
+
content = fs.readFileSync(mdPath, 'utf8');
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return 'active';
|
|
901
|
+
}
|
|
902
|
+
const m = /^status:\s*(\w+)/m.exec(content);
|
|
903
|
+
if (!m)
|
|
904
|
+
return 'active';
|
|
905
|
+
return m[1] === 'skip' ? 'skip' : 'active';
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* When `--filter` narrows to exactly one service, return that service name
|
|
909
|
+
* so `runBrunoCollection` can narrow Bruno CLI's run to that subdir (faster).
|
|
910
|
+
* When multiple services match (or none), return null — run the whole
|
|
911
|
+
* collection and filter results after.
|
|
912
|
+
*/
|
|
913
|
+
function pickFilterService(filterRe, endpoints) {
|
|
914
|
+
if (!filterRe)
|
|
915
|
+
return null;
|
|
916
|
+
const services = new Set();
|
|
917
|
+
for (const e of endpoints) {
|
|
918
|
+
if (filterRe.test(e.service)) {
|
|
919
|
+
services.add(e.service);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (services.size === 1) {
|
|
923
|
+
return [...services][0] ?? null;
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Judge pass/fail for one Bruno run result against the profile's biz-code rule.
|
|
929
|
+
*
|
|
930
|
+
* - Bruno-reported error (timeout / request_failed) → passed=false, error
|
|
931
|
+
* forwarded.
|
|
932
|
+
* - HTTP 5xx → passed=false, error=http_5xx.
|
|
933
|
+
* - HTTP 4xx → passed=false, error=http_4xx.
|
|
934
|
+
* - Other non-200 → passed=false, error=http_non_200.
|
|
935
|
+
* - HTTP 200 + bizCodeField null → passed=true (HTTP-only check).
|
|
936
|
+
* - HTTP 200 + bizCodeField set + value in bizCodeSuccessValues → passed=true.
|
|
937
|
+
* - HTTP 200 + bizCodeField set + value NOT in success values → passed=false,
|
|
938
|
+
* error=biz_code_value_mismatch.
|
|
939
|
+
* - HTTP 200 + bizCodeField set + field missing from response body →
|
|
940
|
+
* passed=false, error=biz_code_field_missing.
|
|
941
|
+
*/
|
|
942
|
+
function judgePass(result, profile) {
|
|
943
|
+
// Bruno-reported errors take precedence (timeout, request_failed, etc.).
|
|
944
|
+
if (result.error) {
|
|
945
|
+
return { passed: false, bizCode: null, error: result.error };
|
|
946
|
+
}
|
|
947
|
+
// Non-200 HTTP statuses.
|
|
948
|
+
if (result.httpStatus >= 500) {
|
|
949
|
+
return { passed: false, bizCode: null, error: 'http_5xx' };
|
|
950
|
+
}
|
|
951
|
+
if (result.httpStatus >= 400) {
|
|
952
|
+
return { passed: false, bizCode: null, error: 'http_4xx' };
|
|
953
|
+
}
|
|
954
|
+
if (result.httpStatus !== 200) {
|
|
955
|
+
return { passed: false, bizCode: null, error: 'http_non_200' };
|
|
956
|
+
}
|
|
957
|
+
// HTTP 200 — check biz code if configured.
|
|
958
|
+
if (profile.bizCodeField === null) {
|
|
959
|
+
return { passed: true, bizCode: null, error: null };
|
|
960
|
+
}
|
|
961
|
+
const bizCode = extractBizCode(result.responseBody, profile.bizCodeField);
|
|
962
|
+
if (bizCode === null) {
|
|
963
|
+
return { passed: false, bizCode: null, error: 'biz_code_field_missing' };
|
|
964
|
+
}
|
|
965
|
+
if (profile.bizCodeSuccessValues.includes(bizCode)) {
|
|
966
|
+
return { passed: true, bizCode, error: null };
|
|
967
|
+
}
|
|
968
|
+
return { passed: false, bizCode, error: 'biz_code_value_mismatch' };
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Extract the biz code value from a response body. The body may be a JSON
|
|
972
|
+
* object or a JSON string. Returns the stringified value, or null when the
|
|
973
|
+
* field is absent / the body can't be parsed.
|
|
974
|
+
*/
|
|
975
|
+
function extractBizCode(responseBody, bizCodeField) {
|
|
976
|
+
if (!responseBody)
|
|
977
|
+
return null;
|
|
978
|
+
// Try parsing as JSON first.
|
|
979
|
+
let parsed;
|
|
980
|
+
try {
|
|
981
|
+
parsed = JSON.parse(responseBody);
|
|
982
|
+
}
|
|
983
|
+
catch {
|
|
984
|
+
// Not JSON — can't extract a structured field.
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
988
|
+
const v = parsed[bizCodeField];
|
|
989
|
+
if (v === undefined || v === null)
|
|
990
|
+
return null;
|
|
991
|
+
return String(v);
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
// Internal helpers re-exported for tests.
|
|
996
|
+
exports.__INTERNAL_CLI = {
|
|
997
|
+
DEFAULT_OUTPUT_DIR,
|
|
998
|
+
BRUNO_COLLECTION_CONFIG,
|
|
999
|
+
clearOutputDir,
|
|
1000
|
+
iterJavaFilesCached,
|
|
1001
|
+
analyzeEndpoint,
|
|
1002
|
+
countParamsInSignature,
|
|
1003
|
+
findImplMethod,
|
|
1004
|
+
readEndpointStatus,
|
|
1005
|
+
pickFilterService,
|
|
1006
|
+
judgePass,
|
|
1007
|
+
extractBizCode,
|
|
1008
|
+
};
|
|
1009
|
+
//# sourceMappingURL=cli.js.map
|