runspec-node 0.27.0 → 0.28.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/cli.d.ts +22 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +143 -0
- package/dist/cli.js.map +1 -1
- package/dist/logging_setup.d.ts.map +1 -1
- package/dist/logging_setup.js +6 -4
- package/dist/logging_setup.js.map +1 -1
- package/dist/runspec.toml +15 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +1 -0
- package/dist/serve.js.map +1 -1
- package/dist/testing.d.ts +67 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +324 -0
- package/dist/testing.js.map +1 -0
- package/package.json +11 -1
- package/src/cli.ts +160 -0
- package/src/logging_setup.ts +6 -4
- package/src/runspec.toml +15 -0
- package/src/serve.ts +1 -1
- package/src/testing.ts +334 -0
- package/tests/test_cli_test.test.ts +134 -0
- package/tests/test_run_summary.test.ts +8 -5
- package/tests/testing.test.ts +132 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runspec-node/testing — test helpers for runspec runnables.
|
|
3
|
+
*
|
|
4
|
+
* The Node port of Python's `runspec.testing`. Import it from the subpath
|
|
5
|
+
* export so it stays out of the main entry:
|
|
6
|
+
*
|
|
7
|
+
* import { ParseHarness } from 'runspec-node/testing';
|
|
8
|
+
*
|
|
9
|
+
* `parse()` already takes `argv` and `configPath`, so testing a runnable never
|
|
10
|
+
* requires monkeypatching `process.argv` or mocking the parser. `ParseHarness`
|
|
11
|
+
* removes the remaining boilerplate — pointing `parse()` at a spec, asserting on
|
|
12
|
+
* clean exits without the process actually exiting, and walking the subcommand
|
|
13
|
+
* tree to auto-cover every command.
|
|
14
|
+
*
|
|
15
|
+
* Node specifics this handles for you: `parse()` calls `process.exit(0)` on
|
|
16
|
+
* `--help` and *throws* `RunSpecError` on validation/require-command errors. The
|
|
17
|
+
* harness intercepts `process.exit` (so `--help` doesn't kill your test runner)
|
|
18
|
+
* and maps a thrown `RunSpecError` to a non-zero "exit", unifying both under one
|
|
19
|
+
* exit-code abstraction that mirrors Python's `SystemExit`-based helpers.
|
|
20
|
+
*/
|
|
21
|
+
import type { ParsedArgs, ArgSpec } from './models';
|
|
22
|
+
export interface ParseHarnessOptions {
|
|
23
|
+
scriptName: string;
|
|
24
|
+
/** An existing runspec.toml (a fixture, or your package's real spec). */
|
|
25
|
+
configPath?: string;
|
|
26
|
+
/** An inline TOML string written to a throwaway temp file. */
|
|
27
|
+
toml?: string;
|
|
28
|
+
/** Pin the per-run summary off during a parse (default true). */
|
|
29
|
+
suppressSummary?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ExpectExitOptions {
|
|
32
|
+
/** Expected exit code; `null` accepts any non-zero. Default 1; use 0 for --help. */
|
|
33
|
+
code?: number | null;
|
|
34
|
+
/** Substring(s) the captured output must include. */
|
|
35
|
+
contains?: string | string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SmokeOptions {
|
|
38
|
+
/** Arg-name → value map used to build valid invocations for each leaf. */
|
|
39
|
+
values?: Record<string, unknown>;
|
|
40
|
+
/** Assert `--help` exits 0 for the runnable and every subcommand (default true). */
|
|
41
|
+
checkHelp?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare class ParseHarness {
|
|
44
|
+
readonly scriptName: string;
|
|
45
|
+
readonly configPath: string;
|
|
46
|
+
private readonly suppressSummary;
|
|
47
|
+
private tmpDir;
|
|
48
|
+
private readonly spec;
|
|
49
|
+
constructor(opts: ParseHarnessOptions);
|
|
50
|
+
close(): void;
|
|
51
|
+
parse(argv: string[]): ParsedArgs;
|
|
52
|
+
expectOk(argv: string[]): ParsedArgs;
|
|
53
|
+
expectExit(argv: string[], opts?: ExpectExitOptions): string;
|
|
54
|
+
commandPaths(): string[][];
|
|
55
|
+
leafCommandPaths(): string[][];
|
|
56
|
+
argsFor(p?: string[]): Record<string, ArgSpec>;
|
|
57
|
+
requiredArgs(p?: string[]): string[];
|
|
58
|
+
requiresCommand(p?: string[]): boolean;
|
|
59
|
+
smoke(opts?: SmokeOptions): string[];
|
|
60
|
+
private runParse;
|
|
61
|
+
private node;
|
|
62
|
+
private mergedArgs;
|
|
63
|
+
private buildValidArgv;
|
|
64
|
+
private withSummaryPinned;
|
|
65
|
+
}
|
|
66
|
+
export declare function harness(scriptName: string, opts?: Omit<ParseHarnessOptions, 'scriptName'>): ParseHarness;
|
|
67
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,OAAO,KAAK,EAAE,UAAU,EAAc,OAAO,EAAE,MAAM,UAAU,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AASD,qBAAa,YAAY;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;gBAEtB,IAAI,EAAE,mBAAmB;IAkCrC,KAAK,IAAI,IAAI;IASb,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU;IAQjC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU;IAIpC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM;IAsBhE,YAAY,IAAI,MAAM,EAAE,EAAE;IAa1B,gBAAgB,IAAI,MAAM,EAAE,EAAE;IAI9B,OAAO,CAAC,CAAC,GAAE,MAAM,EAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAIlD,YAAY,CAAC,CAAC,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE;IAMxC,eAAe,CAAC,CAAC,GAAE,MAAM,EAAO,GAAG,OAAO;IAM1C,KAAK,CAAC,IAAI,GAAE,YAAiB,GAAG,MAAM,EAAE;IAgDxC,OAAO,CAAC,QAAQ;IAwDhB,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,iBAAiB;CAa1B;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,CAAM,GAAG,YAAY,CAE5G"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* runspec-node/testing — test helpers for runspec runnables.
|
|
4
|
+
*
|
|
5
|
+
* The Node port of Python's `runspec.testing`. Import it from the subpath
|
|
6
|
+
* export so it stays out of the main entry:
|
|
7
|
+
*
|
|
8
|
+
* import { ParseHarness } from 'runspec-node/testing';
|
|
9
|
+
*
|
|
10
|
+
* `parse()` already takes `argv` and `configPath`, so testing a runnable never
|
|
11
|
+
* requires monkeypatching `process.argv` or mocking the parser. `ParseHarness`
|
|
12
|
+
* removes the remaining boilerplate — pointing `parse()` at a spec, asserting on
|
|
13
|
+
* clean exits without the process actually exiting, and walking the subcommand
|
|
14
|
+
* tree to auto-cover every command.
|
|
15
|
+
*
|
|
16
|
+
* Node specifics this handles for you: `parse()` calls `process.exit(0)` on
|
|
17
|
+
* `--help` and *throws* `RunSpecError` on validation/require-command errors. The
|
|
18
|
+
* harness intercepts `process.exit` (so `--help` doesn't kill your test runner)
|
|
19
|
+
* and maps a thrown `RunSpecError` to a non-zero "exit", unifying both under one
|
|
20
|
+
* exit-code abstraction that mirrors Python's `SystemExit`-based helpers.
|
|
21
|
+
*/
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.ParseHarness = void 0;
|
|
57
|
+
exports.harness = harness;
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const os = __importStar(require("os"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const parser_1 = require("./parser");
|
|
62
|
+
const loader_1 = require("./loader");
|
|
63
|
+
const inference_1 = require("./inference");
|
|
64
|
+
const errors_1 = require("./errors");
|
|
65
|
+
/** Thrown in place of `process.exit()` so a parse that exits can be caught. */
|
|
66
|
+
class ExitSignal extends Error {
|
|
67
|
+
exitCode;
|
|
68
|
+
constructor(exitCode) {
|
|
69
|
+
super(`process.exit(${exitCode})`);
|
|
70
|
+
this.exitCode = exitCode;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class ParseHarness {
|
|
74
|
+
scriptName;
|
|
75
|
+
configPath;
|
|
76
|
+
suppressSummary;
|
|
77
|
+
tmpDir = null;
|
|
78
|
+
spec;
|
|
79
|
+
constructor(opts) {
|
|
80
|
+
const { scriptName, configPath, toml, suppressSummary = true } = opts;
|
|
81
|
+
if ((configPath == null) === (toml == null)) {
|
|
82
|
+
throw new Error('pass exactly one of configPath or toml');
|
|
83
|
+
}
|
|
84
|
+
this.scriptName = scriptName;
|
|
85
|
+
this.suppressSummary = suppressSummary;
|
|
86
|
+
let cfgPath = configPath;
|
|
87
|
+
if (toml != null) {
|
|
88
|
+
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'runspec-test-'));
|
|
89
|
+
cfgPath = path.join(this.tmpDir, 'runspec.toml');
|
|
90
|
+
fs.writeFileSync(cfgPath, toml, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
this.configPath = path.resolve(cfgPath);
|
|
93
|
+
try {
|
|
94
|
+
const raw = (0, loader_1.loadRaw)(this.configPath);
|
|
95
|
+
if (!(scriptName in raw.runnables)) {
|
|
96
|
+
const have = Object.keys(raw.runnables).sort().join(', ') || '(none)';
|
|
97
|
+
throw new Error(`runnable '${scriptName}' not in ${this.configPath} (have: ${have})`);
|
|
98
|
+
}
|
|
99
|
+
// Infer so `required` is resolved from defaults/types (raw leaves it unset).
|
|
100
|
+
this.spec = (0, inference_1.inferScript)(raw.runnables[scriptName], raw.config.autonomyDefault);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
// Tear down the temp dir synchronously on a failed construction.
|
|
104
|
+
this.close();
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ── lifecycle ─────────────────────────────────────────────────────────────
|
|
109
|
+
close() {
|
|
110
|
+
if (this.tmpDir != null) {
|
|
111
|
+
fs.rmSync(this.tmpDir, { recursive: true, force: true });
|
|
112
|
+
this.tmpDir = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── core parse entry points ───────────────────────────────────────────────
|
|
116
|
+
parse(argv) {
|
|
117
|
+
const { code, output, parsed } = this.runParse(argv);
|
|
118
|
+
if (parsed === null) {
|
|
119
|
+
throw new Error(`expected \`${this.scriptName} ${argv.join(' ')}\` to parse, but it exited ${code}:\n${output}`);
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
expectOk(argv) {
|
|
124
|
+
return this.parse(argv);
|
|
125
|
+
}
|
|
126
|
+
expectExit(argv, opts = {}) {
|
|
127
|
+
const { code = 1, contains } = opts;
|
|
128
|
+
const res = this.runParse(argv);
|
|
129
|
+
if (res.code === null) {
|
|
130
|
+
throw new Error(`expected \`${this.scriptName} ${argv.join(' ')}\` to exit, but it parsed cleanly`);
|
|
131
|
+
}
|
|
132
|
+
if (code !== null && res.code !== code) {
|
|
133
|
+
throw new Error(`\`${this.scriptName} ${argv.join(' ')}\`: expected exit ${code}, got ${res.code}:\n${res.output}`);
|
|
134
|
+
}
|
|
135
|
+
if (contains != null) {
|
|
136
|
+
const needles = Array.isArray(contains) ? contains : [contains];
|
|
137
|
+
for (const needle of needles) {
|
|
138
|
+
if (!res.output.includes(needle)) {
|
|
139
|
+
throw new Error(`\`${this.scriptName} ${argv.join(' ')}\`: ${JSON.stringify(needle)} not found in output:\n${res.output}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return res.output;
|
|
144
|
+
}
|
|
145
|
+
// ── spec introspection ────────────────────────────────────────────────────
|
|
146
|
+
commandPaths() {
|
|
147
|
+
const paths = [];
|
|
148
|
+
const walk = (node, prefix) => {
|
|
149
|
+
for (const [name, sub] of Object.entries(node.commands ?? {})) {
|
|
150
|
+
const here = [...prefix, name];
|
|
151
|
+
paths.push(here);
|
|
152
|
+
walk(sub, here);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
walk(this.spec, []);
|
|
156
|
+
return paths;
|
|
157
|
+
}
|
|
158
|
+
leafCommandPaths() {
|
|
159
|
+
return this.commandPaths().filter((p) => Object.keys(this.node(p).commands ?? {}).length === 0);
|
|
160
|
+
}
|
|
161
|
+
argsFor(p = []) {
|
|
162
|
+
return this.node(p).args ?? {};
|
|
163
|
+
}
|
|
164
|
+
requiredArgs(p = []) {
|
|
165
|
+
return Object.entries(this.argsFor(p))
|
|
166
|
+
.filter(([, a]) => a.required)
|
|
167
|
+
.map(([name]) => name);
|
|
168
|
+
}
|
|
169
|
+
requiresCommand(p = []) {
|
|
170
|
+
return Boolean(this.node(p).requireCommand);
|
|
171
|
+
}
|
|
172
|
+
// ── auto coverage ─────────────────────────────────────────────────────────
|
|
173
|
+
smoke(opts = {}) {
|
|
174
|
+
const { values = {}, checkHelp = true } = opts;
|
|
175
|
+
const done = [];
|
|
176
|
+
if (checkHelp) {
|
|
177
|
+
this.expectExit(['--help'], { code: 0 });
|
|
178
|
+
done.push('--help');
|
|
179
|
+
}
|
|
180
|
+
if (this.requiresCommand()) {
|
|
181
|
+
this.expectExit([]);
|
|
182
|
+
done.push('(no command) -> require-command');
|
|
183
|
+
}
|
|
184
|
+
for (const p of this.commandPaths()) {
|
|
185
|
+
const label = p.join(' ');
|
|
186
|
+
const node = this.node(p);
|
|
187
|
+
if (checkHelp) {
|
|
188
|
+
this.expectExit([...p, '--help'], { code: 0 });
|
|
189
|
+
done.push(`${label} --help`);
|
|
190
|
+
}
|
|
191
|
+
if (node.requireCommand) {
|
|
192
|
+
this.expectExit(p);
|
|
193
|
+
done.push(`${label} -> require-command`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (Object.keys(node.commands ?? {}).length > 0) {
|
|
197
|
+
continue; // intermediate group without require-command
|
|
198
|
+
}
|
|
199
|
+
if (this.requiredArgs(p).length > 0) {
|
|
200
|
+
this.expectExit(p);
|
|
201
|
+
done.push(`${label} -> missing required`);
|
|
202
|
+
}
|
|
203
|
+
const built = this.buildValidArgv(p, values);
|
|
204
|
+
if (built !== null) {
|
|
205
|
+
this.expectOk([...p, ...built]);
|
|
206
|
+
done.push(`${label} (valid)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return done;
|
|
210
|
+
}
|
|
211
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
212
|
+
runParse(argv) {
|
|
213
|
+
// Capture the *original* (unbound) handles so restoration preserves identity.
|
|
214
|
+
const realExit = process.exit;
|
|
215
|
+
const realOut = process.stdout.write;
|
|
216
|
+
const realErr = process.stderr.write;
|
|
217
|
+
const realLog = console.log;
|
|
218
|
+
const realErrLog = console.error;
|
|
219
|
+
let captured = '';
|
|
220
|
+
process.exit = (c) => {
|
|
221
|
+
throw new ExitSignal(c ?? 0);
|
|
222
|
+
};
|
|
223
|
+
process.stdout.write = ((chunk) => {
|
|
224
|
+
captured += String(chunk);
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
process.stderr.write = ((chunk) => {
|
|
228
|
+
captured += String(chunk);
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
console.log = (...a) => {
|
|
232
|
+
captured += a.map(String).join(' ') + '\n';
|
|
233
|
+
};
|
|
234
|
+
console.error = (...a) => {
|
|
235
|
+
captured += a.map(String).join(' ') + '\n';
|
|
236
|
+
};
|
|
237
|
+
return this.withSummaryPinned(() => {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = (0, parser_1.parse)({
|
|
240
|
+
scriptName: this.scriptName,
|
|
241
|
+
argv,
|
|
242
|
+
configPath: this.configPath,
|
|
243
|
+
_promptSecrets: false,
|
|
244
|
+
});
|
|
245
|
+
return { code: null, output: captured, parsed };
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
if (e instanceof ExitSignal) {
|
|
249
|
+
return { code: e.exitCode, output: captured, parsed: null };
|
|
250
|
+
}
|
|
251
|
+
if (e instanceof errors_1.RunSpecError) {
|
|
252
|
+
// Validation / require-command throw rather than exit; fold the
|
|
253
|
+
// message into the captured output so `contains:` can match on it.
|
|
254
|
+
return { code: 1, output: `${captured}${e.message}\n`, parsed: null };
|
|
255
|
+
}
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
process.exit = realExit;
|
|
260
|
+
process.stdout.write = realOut;
|
|
261
|
+
process.stderr.write = realErr;
|
|
262
|
+
console.log = realLog;
|
|
263
|
+
console.error = realErrLog;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
node(p) {
|
|
268
|
+
let node = this.spec;
|
|
269
|
+
for (const part of p)
|
|
270
|
+
node = (node.commands ?? {})[part];
|
|
271
|
+
return node;
|
|
272
|
+
}
|
|
273
|
+
mergedArgs(p) {
|
|
274
|
+
const merged = { ...this.argsFor([]) };
|
|
275
|
+
const acc = [];
|
|
276
|
+
for (const part of p) {
|
|
277
|
+
acc.push(part);
|
|
278
|
+
Object.assign(merged, this.argsFor(acc));
|
|
279
|
+
}
|
|
280
|
+
return merged;
|
|
281
|
+
}
|
|
282
|
+
buildValidArgv(p, values) {
|
|
283
|
+
const argv = [];
|
|
284
|
+
for (const [name, a] of Object.entries(this.mergedArgs(p))) {
|
|
285
|
+
const supplied = values[name];
|
|
286
|
+
if (supplied === undefined || supplied === null) {
|
|
287
|
+
if (a.required)
|
|
288
|
+
return null;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const flag = `--${name}`;
|
|
292
|
+
if (a.type === 'flag') {
|
|
293
|
+
if (supplied)
|
|
294
|
+
argv.push(flag);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
argv.push(flag, String(supplied));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return argv;
|
|
301
|
+
}
|
|
302
|
+
withSummaryPinned(fn) {
|
|
303
|
+
if (!this.suppressSummary)
|
|
304
|
+
return fn();
|
|
305
|
+
const prefix = this.scriptName.toUpperCase().replace(/-/g, '_');
|
|
306
|
+
const key = `RUNSPEC_${prefix}_ARG_NO_SUMMARY`;
|
|
307
|
+
const prev = process.env[key];
|
|
308
|
+
process.env[key] = '1';
|
|
309
|
+
try {
|
|
310
|
+
return fn();
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
if (prev === undefined)
|
|
314
|
+
delete process.env[key];
|
|
315
|
+
else
|
|
316
|
+
process.env[key] = prev;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
exports.ParseHarness = ParseHarness;
|
|
321
|
+
function harness(scriptName, opts = {}) {
|
|
322
|
+
return new ParseHarness({ scriptName, ...opts });
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runspec-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Node/TypeScript language pack for runspec",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./testing": {
|
|
13
|
+
"types": "./dist/testing.d.ts",
|
|
14
|
+
"default": "./dist/testing.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"bin": {
|
|
8
18
|
"runspec": "./bin/runspec.js"
|
|
9
19
|
},
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
4
5
|
import { findConfig } from './finder';
|
|
5
6
|
import * as logs from './logs';
|
|
6
7
|
import { loadRaw } from './loader';
|
|
@@ -33,6 +34,7 @@ export function main(): void {
|
|
|
33
34
|
const commands: Record<string, (args: string[]) => void | Promise<void>> = {
|
|
34
35
|
init: cmdInit,
|
|
35
36
|
local: cmdLocal,
|
|
37
|
+
test: cmdTest,
|
|
36
38
|
bin: cmdBin,
|
|
37
39
|
logs: cmdLogs,
|
|
38
40
|
jump: cmdJump,
|
|
@@ -359,6 +361,145 @@ function cmdServe(_args: string[]): void {
|
|
|
359
361
|
serve();
|
|
360
362
|
}
|
|
361
363
|
|
|
364
|
+
// Hard cap on the entry-point `--help` subprocess so a runnable that hangs on
|
|
365
|
+
// import can't wedge a CI run. A timeout is treated as a failure.
|
|
366
|
+
const TEST_SUBPROCESS_TIMEOUT_MS = 30000;
|
|
367
|
+
|
|
368
|
+
interface TestCheck {
|
|
369
|
+
ok: boolean;
|
|
370
|
+
checks?: string[];
|
|
371
|
+
error?: string;
|
|
372
|
+
stderr?: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
interface TestResult {
|
|
376
|
+
runnable: string;
|
|
377
|
+
source: string;
|
|
378
|
+
ok: boolean;
|
|
379
|
+
checks: { spec_smoke: TestCheck; entry_point: TestCheck };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function cmdTest(args: string[]): void {
|
|
383
|
+
const fmt = getFlag(args, '--format') ?? 'text';
|
|
384
|
+
const runnableFilter = getFlag(args, '--runnable');
|
|
385
|
+
|
|
386
|
+
let discovered = discoverLocal();
|
|
387
|
+
if (runnableFilter) discovered = discovered.filter((d) => d.runnable === runnableFilter);
|
|
388
|
+
|
|
389
|
+
if (!discovered.length) {
|
|
390
|
+
console.log('No runspec-aware runnables found in this environment.');
|
|
391
|
+
console.log("Run from a folder with a runspec.toml, or 'runspec init' to create one.");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const results = discovered.map((d) => testOne(d));
|
|
396
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
397
|
+
|
|
398
|
+
if (fmt === 'json') {
|
|
399
|
+
const summary = { total: results.length, passed: results.length - failed, failed };
|
|
400
|
+
console.log(JSON.stringify({ results, summary }, null, 2));
|
|
401
|
+
} else {
|
|
402
|
+
printTestText(results);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (failed) process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Run both checks against one discovered runnable; never throws. Each phase is
|
|
409
|
+
// isolated so a single broken runnable cannot abort the run.
|
|
410
|
+
export function testOne(item: { source: string; runnable: string; spec: ScriptSpec }): TestResult {
|
|
411
|
+
const { runnable: name, source } = item;
|
|
412
|
+
|
|
413
|
+
// ── phase 1: spec smoke (in-process) ──────────────────────────────────────
|
|
414
|
+
let specSmoke: TestCheck;
|
|
415
|
+
try {
|
|
416
|
+
const { ParseHarness } = require('./testing') as typeof import('./testing');
|
|
417
|
+
const h = new ParseHarness({ scriptName: name, configPath: source, suppressSummary: true });
|
|
418
|
+
try {
|
|
419
|
+
specSmoke = { ok: true, checks: h.smoke({ checkHelp: true }) };
|
|
420
|
+
} finally {
|
|
421
|
+
h.close();
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {
|
|
424
|
+
specSmoke = { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── phase 2: entry-point --help (subprocess) ──────────────────────────────
|
|
428
|
+
const { findScript } = require('./serve') as typeof import('./serve');
|
|
429
|
+
const binDir = path.join(path.dirname(source), 'node_modules', '.bin');
|
|
430
|
+
const cmd = findScript(name, binDir);
|
|
431
|
+
|
|
432
|
+
let entryPoint: TestCheck;
|
|
433
|
+
if (cmd === null) {
|
|
434
|
+
entryPoint = { ok: false, error: 'entry point not found — run `runspec bin` or check package.json "bin"' };
|
|
435
|
+
} else {
|
|
436
|
+
const res = spawnSync(cmd, ['--help'], {
|
|
437
|
+
encoding: 'utf-8',
|
|
438
|
+
timeout: TEST_SUBPROCESS_TIMEOUT_MS,
|
|
439
|
+
env: { ...process.env, RUNSPEC_AGENT: '1' },
|
|
440
|
+
});
|
|
441
|
+
if (res.error) {
|
|
442
|
+
const code = (res.error as NodeJS.ErrnoException).code;
|
|
443
|
+
const msg = code === 'ETIMEDOUT' ? `timed out after ${TEST_SUBPROCESS_TIMEOUT_MS / 1000}s` : res.error.message;
|
|
444
|
+
entryPoint = { ok: false, error: msg };
|
|
445
|
+
} else if (res.status !== 0) {
|
|
446
|
+
entryPoint = {
|
|
447
|
+
ok: false,
|
|
448
|
+
error: `exit ${res.status}`,
|
|
449
|
+
stderr: (res.stderr || res.stdout || '').trim().slice(0, 2000),
|
|
450
|
+
};
|
|
451
|
+
} else {
|
|
452
|
+
entryPoint = { ok: true };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
runnable: name,
|
|
458
|
+
source,
|
|
459
|
+
ok: specSmoke.ok && entryPoint.ok,
|
|
460
|
+
checks: { spec_smoke: specSmoke, entry_point: entryPoint },
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Pick the most informative single line from captured stderr: the last line
|
|
465
|
+
// mentioning an error/exception (the "Error: ..." line for a Node crash, the
|
|
466
|
+
// exception for a Python traceback), falling back to the last non-empty line.
|
|
467
|
+
function summariseStderr(stderr: string): string {
|
|
468
|
+
const lines = stderr
|
|
469
|
+
.split('\n')
|
|
470
|
+
.map((l) => l.trim())
|
|
471
|
+
.filter((l) => l);
|
|
472
|
+
if (!lines.length) return '';
|
|
473
|
+
const errLines = lines.filter((l) => /error|exception/i.test(l));
|
|
474
|
+
return (errLines.length ? errLines : lines)[(errLines.length ? errLines : lines).length - 1];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function printTestText(results: TestResult[]): void {
|
|
478
|
+
const total = results.length;
|
|
479
|
+
const passed = results.filter((r) => r.ok).length;
|
|
480
|
+
const failed = total - passed;
|
|
481
|
+
|
|
482
|
+
console.log(`Tested ${total} runnable(s):\n`);
|
|
483
|
+
for (const r of results) {
|
|
484
|
+
const spec = r.checks.spec_smoke;
|
|
485
|
+
const ep = r.checks.entry_point;
|
|
486
|
+
const mark = r.ok ? '✓' : '✗';
|
|
487
|
+
const specNote = spec.ok ? 'spec ok' : 'spec failed';
|
|
488
|
+
const epNote = ep.ok ? '--help exit 0' : '--help failed';
|
|
489
|
+
console.log(` ${mark} ${r.runnable.padEnd(24)} ${specNote}, ${epNote}`);
|
|
490
|
+
if (!spec.ok) console.log(` spec: ${spec.error}`);
|
|
491
|
+
if (!ep.ok) {
|
|
492
|
+
console.log(` entry point: ${ep.error}`);
|
|
493
|
+
if (ep.stderr) {
|
|
494
|
+
const summary = summariseStderr(ep.stderr);
|
|
495
|
+
if (summary) console.log(` stderr: ${summary}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
console.log();
|
|
500
|
+
console.log(`Summary: ${passed} passed, ${failed} failed (${total} total)`);
|
|
501
|
+
}
|
|
502
|
+
|
|
362
503
|
// ── Schema builder ────────────────────────────────────────────────────────────
|
|
363
504
|
|
|
364
505
|
export function buildSchema(name: string, script: ScriptSpec, fmt: string): Record<string, unknown> {
|
|
@@ -830,6 +971,7 @@ Usage:
|
|
|
830
971
|
Commands:
|
|
831
972
|
init Create runspec.toml and a code stub
|
|
832
973
|
local List runnables and emit tool schemas
|
|
974
|
+
test Smoke-test every runnable: spec smoke + entry-point --help (CI gate)
|
|
833
975
|
bin Generate a venv-shaped bin/ so a controller can run this folder
|
|
834
976
|
logs View, status, prune, or compact per-invocation audit logs
|
|
835
977
|
jump Execute a runnable on a remote host via SSH
|
|
@@ -842,6 +984,7 @@ Examples:
|
|
|
842
984
|
runspec init --example
|
|
843
985
|
runspec local
|
|
844
986
|
runspec local --format mcp
|
|
987
|
+
runspec test
|
|
845
988
|
runspec bin
|
|
846
989
|
runspec logs deploy
|
|
847
990
|
runspec serve`);
|
|
@@ -874,6 +1017,23 @@ Examples:
|
|
|
874
1017
|
runspec local --format mcp --script deploy
|
|
875
1018
|
runspec local --format json`,
|
|
876
1019
|
|
|
1020
|
+
test: `runspec test — Smoke-test every runnable in this folder (CI gate)
|
|
1021
|
+
|
|
1022
|
+
For each runnable in runspec.toml runs two checks: an in-process spec smoke
|
|
1023
|
+
(--help works for every command, require-command is enforced, required args
|
|
1024
|
+
are validated) and a subprocess that executes the entry point with --help
|
|
1025
|
+
(proves the runnable's own code imports and is wired to the right spec).
|
|
1026
|
+
Exits 1 if any runnable fails either check.
|
|
1027
|
+
|
|
1028
|
+
Options:
|
|
1029
|
+
--format Output format: text (default) or json
|
|
1030
|
+
--runnable Filter to a single runnable by name
|
|
1031
|
+
|
|
1032
|
+
Examples:
|
|
1033
|
+
runspec test
|
|
1034
|
+
runspec test --format json
|
|
1035
|
+
runspec test --runnable deploy`,
|
|
1036
|
+
|
|
877
1037
|
bin: `runspec bin — Generate a venv-shaped bin/ for this folder's runnables
|
|
878
1038
|
|
|
879
1039
|
Writes bin/runspec plus one bin/<runnable> shim per runnable in runspec.toml,
|
package/src/logging_setup.ts
CHANGED
|
@@ -490,16 +490,18 @@ function formatSummaryLine(state: SummaryState, durationMs: number, exitCode: nu
|
|
|
490
490
|
const errors = (counts.ERROR ?? 0) + (counts.CRITICAL ?? 0);
|
|
491
491
|
const secs = (durationMs / 1000).toFixed(2);
|
|
492
492
|
const runnable = state.runnable;
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
// Abbreviated level words ("warn"/"err", never "warning"/"error") so the line
|
|
494
|
+
// doesn't trip Rundeck's case-insensitive `error` log-highlight/level filters,
|
|
495
|
+
// which would otherwise paint a clean run red over the literal "0 errors".
|
|
496
|
+
const events = `${total} events (${warnings} warn, ${errors} err)`;
|
|
495
497
|
const userPart = state.userTarget
|
|
496
498
|
? ` | user: ${state.user} → ${state.userTarget} (sudo)`
|
|
497
499
|
: ` | user: ${state.user}`;
|
|
498
500
|
if (state.exception || exitCode !== 0) {
|
|
499
501
|
const excPart = state.exception ? `, ${state.exception.type}` : '';
|
|
500
|
-
return `runspec: ${runnable} failed in ${secs}s — exit ${exitCode}${excPart} — ${
|
|
502
|
+
return `runspec: ${runnable} failed in ${secs}s — exit ${exitCode}${excPart} — ${events}${userPart}`;
|
|
501
503
|
}
|
|
502
|
-
return `runspec: ${runnable} completed in ${secs}s — ${
|
|
504
|
+
return `runspec: ${runnable} completed in ${secs}s — ${events}${userPart}`;
|
|
503
505
|
}
|
|
504
506
|
|
|
505
507
|
/**
|