@glubean/cli 0.4.0 → 0.7.0
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/commands/contracts.d.ts +1 -4
- package/dist/commands/contracts.d.ts.map +1 -1
- package/dist/commands/contracts.js +11 -188
- package/dist/commands/contracts.js.map +1 -1
- package/dist/commands/migrate.js +3 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/run.d.ts +3 -24
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +111 -89
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +5 -1
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/validate_metadata.d.ts.map +1 -1
- package/dist/commands/validate_metadata.js +5 -1
- package/dist/commands/validate_metadata.js.map +1 -1
- package/dist/lib/redact-metadata.d.ts +47 -0
- package/dist/lib/redact-metadata.d.ts.map +1 -0
- package/dist/lib/redact-metadata.js +84 -0
- package/dist/lib/redact-metadata.js.map +1 -0
- package/dist/lib/upload.d.ts +20 -1
- package/dist/lib/upload.d.ts.map +1 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/metadata.d.ts +18 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +48 -2
- package/dist/metadata.js.map +1 -1
- package/package.json +9 -6
package/dist/commands/run.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadProjectEnv } from "@glubean/runner";
|
|
|
7
7
|
import { resolveEnvFileName } from "../lib/active_env.js";
|
|
8
8
|
import { shouldSkipTest } from "../lib/skip.js";
|
|
9
9
|
import { CLI_VERSION } from "../version.js";
|
|
10
|
+
import { redactMetadataForUpload } from "../lib/redact-metadata.js";
|
|
10
11
|
import { extractContractCases, extractFromSource } from "@glubean/scanner/static";
|
|
11
12
|
import { extractContractFromFile, findTemplateMatch, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
|
|
12
13
|
import { applyEnvTemplating } from "@glubean/runner";
|
|
@@ -49,28 +50,6 @@ async function findProjectConfig(startDir) {
|
|
|
49
50
|
// No glubean project found — use the starting directory (scratch mode)
|
|
50
51
|
return { rootDir: startDir };
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
-
* True if a flow's extracted step tree contains a branch (condition / switch).
|
|
54
|
-
* Recurses into branch cases/default (a nested branch would itself be caught by
|
|
55
|
-
* the top-level `kind === "branch"`, but recursing is cheap belt-and-suspenders).
|
|
56
|
-
*
|
|
57
|
-
* Used to gate `--upload`: Glubean Cloud cannot render `kind:"branch"` flows yet,
|
|
58
|
-
* and uploading would silently drop the branches (local run view ≠ Cloud view),
|
|
59
|
-
* so we refuse rather than mislead. See contract-flow-condition.md §12 / Spike 6.
|
|
60
|
-
*/
|
|
61
|
-
function flowStepsHaveBranch(steps) {
|
|
62
|
-
if (!steps)
|
|
63
|
-
return false;
|
|
64
|
-
for (const s of steps) {
|
|
65
|
-
if (s.kind === "branch")
|
|
66
|
-
return true;
|
|
67
|
-
if (s.cases && s.cases.some((c) => flowStepsHaveBranch(c.steps)))
|
|
68
|
-
return true;
|
|
69
|
-
if (s.default && flowStepsHaveBranch(s.default))
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
53
|
// Config consolidation (docs/06): the package.json `glubean` field is no
|
|
75
54
|
// longer a config source. Warn (don't error) when one lingers so users
|
|
76
55
|
// migrate it into glubean.yaml instead of wondering why it stopped working.
|
|
@@ -101,12 +80,33 @@ function isGlob(target) {
|
|
|
101
80
|
const TEST_FILE_SUFFIXES = [
|
|
102
81
|
".test.ts",
|
|
103
82
|
".contract.ts",
|
|
83
|
+
".workflow.ts",
|
|
104
84
|
".flow.ts",
|
|
105
85
|
".bootstrap.ts",
|
|
106
86
|
];
|
|
107
87
|
function isGlubeanTestFile(name) {
|
|
108
88
|
return TEST_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
109
89
|
}
|
|
90
|
+
// Files that carry contract/workflow DECLARATIONS and are safe to
|
|
91
|
+
// runtime-import for extraction. Matched by SUFFIX, not substring: a normal
|
|
92
|
+
// test like `checkout.workflow.test.ts` ends in `.test.ts` and must stay on
|
|
93
|
+
// the static test path — a `.workflow.` substring would mis-route it into
|
|
94
|
+
// contract extraction (which ignores `test()` exports), silently dropping its
|
|
95
|
+
// tests (codex 0.6 P2).
|
|
96
|
+
const RUNTIME_ARTIFACT_SUFFIXES = [
|
|
97
|
+
".contract.ts",
|
|
98
|
+
".contract.js",
|
|
99
|
+
".contract.mjs",
|
|
100
|
+
".workflow.ts",
|
|
101
|
+
".workflow.js",
|
|
102
|
+
".workflow.mjs",
|
|
103
|
+
".flow.ts",
|
|
104
|
+
".flow.js",
|
|
105
|
+
".flow.mjs",
|
|
106
|
+
];
|
|
107
|
+
function isRuntimeExtractableArtifact(name) {
|
|
108
|
+
return RUNTIME_ARTIFACT_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
109
|
+
}
|
|
110
110
|
function isBootstrapOnlyFile(name) {
|
|
111
111
|
return name.endsWith(".bootstrap.ts");
|
|
112
112
|
}
|
|
@@ -129,7 +129,7 @@ export function classifyGlubeanFile(filePath) {
|
|
|
129
129
|
return "test";
|
|
130
130
|
if (filePath.endsWith(".contract.ts"))
|
|
131
131
|
return "contract";
|
|
132
|
-
if (filePath.endsWith(".flow.ts"))
|
|
132
|
+
if (filePath.endsWith(".workflow.ts") || filePath.endsWith(".flow.ts"))
|
|
133
133
|
return "flow";
|
|
134
134
|
if (filePath.endsWith(".bootstrap.ts"))
|
|
135
135
|
return "bootstrap";
|
|
@@ -229,6 +229,14 @@ export async function resolveTestFilesForSuite(target, kinds) {
|
|
|
229
229
|
// `.flow.ts` file (recommended canonical layout) or declare the
|
|
230
230
|
// suite as `kinds: [contract, flow]` so both candidate file types
|
|
231
231
|
// are scanned and the runnable-level filter sorts them out.
|
|
232
|
+
//
|
|
233
|
+
// The same applies to a vNext workflow authored in a `.test.ts`
|
|
234
|
+
// (workflows ride the "flow" RUNNABLE kind, codex S2.6 R10/R11): a
|
|
235
|
+
// strict `kinds: [flow]` suite won't see the file. Move the workflow
|
|
236
|
+
// to a `.flow.ts` (recommended — it also gains the metadata
|
|
237
|
+
// projection, which `.test.ts` files never get) or declare
|
|
238
|
+
// `kinds: [test, flow]`; the runnable-level filter then keeps the
|
|
239
|
+
// workflow and drops the plain test() exports.
|
|
232
240
|
return files.filter((f) => {
|
|
233
241
|
const k = classifyGlubeanFile(f);
|
|
234
242
|
if (k === undefined)
|
|
@@ -250,16 +258,21 @@ export async function discoverTests(filePath) {
|
|
|
250
258
|
return [];
|
|
251
259
|
}
|
|
252
260
|
const content = await readFile(filePath, "utf-8");
|
|
253
|
-
if (
|
|
261
|
+
if (isRuntimeExtractableArtifact(filePath)) {
|
|
254
262
|
// Runtime extraction via shared function (supports .with() syntax).
|
|
255
|
-
// Returns BOTH contracts and
|
|
256
|
-
//
|
|
257
|
-
// per contract case.
|
|
263
|
+
// Returns BOTH contracts and workflows; `.workflow.ts` / `.flow.ts` files
|
|
264
|
+
// often export only workflows, so we must emit one DiscoveredTest per
|
|
265
|
+
// workflow in addition to per contract case.
|
|
258
266
|
const result = await extractContractFromFile(filePath);
|
|
259
267
|
const results = [];
|
|
260
268
|
for (const ec of result.contracts) {
|
|
261
269
|
const contractTags = ec.tags ?? [];
|
|
262
270
|
for (const c of ec.cases) {
|
|
271
|
+
// Non-runnable cases (direction: "inbound", design §9.5): the SDK
|
|
272
|
+
// registered no Test — advertising them here would schedule an id
|
|
273
|
+
// that can never resolve (codex I2 R1 P1).
|
|
274
|
+
if (c.runnable === false)
|
|
275
|
+
continue;
|
|
263
276
|
// Mirror SDK dispatchContract: finalTags = contract + case + runtime
|
|
264
277
|
// synthetic. Without this, pre-spawn excludeTags / --tag filtering
|
|
265
278
|
// skips contract cases entirely (Phase 1 filter reads meta.tags).
|
|
@@ -291,22 +304,28 @@ export async function discoverTests(filePath) {
|
|
|
291
304
|
});
|
|
292
305
|
}
|
|
293
306
|
}
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
307
|
+
// vNext workflows (S2.6): each BuiltWorkflow wraps the graph in ONE
|
|
308
|
+
// simple test — discover it like a flow orchestrator, so a file whose
|
|
309
|
+
// projection scan/upload advertises is also runnable (codex S2.6 R2 P2).
|
|
310
|
+
// During the migration window workflows ride the "flow" runnable kind:
|
|
311
|
+
// a workflow IS the vNext orchestrator replacing contract.flow(), and
|
|
312
|
+
// suites declaring `kinds: [flow]` mean "run the graph orchestrators in
|
|
313
|
+
// these files" — no new user-facing kinds enum until flow is deleted.
|
|
314
|
+
// WorkflowMeta.skip → deferred mirrors the SDK's own Test wrapping.
|
|
315
|
+
for (const wf of result.workflows) {
|
|
302
316
|
results.push({
|
|
303
|
-
exportName:
|
|
317
|
+
exportName: wf.exportName,
|
|
304
318
|
meta: {
|
|
305
|
-
id:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
319
|
+
id: wf.id,
|
|
320
|
+
name: wf.name,
|
|
321
|
+
description: wf.description,
|
|
322
|
+
tags: wf.tags,
|
|
323
|
+
only: wf.only,
|
|
324
|
+
deferred: wf.skip,
|
|
325
|
+
// data-driven members: grouping + concurrency ride the projection
|
|
326
|
+
// (codex S2.12 R1 P2 — the registry alone never reaches the CLI).
|
|
327
|
+
...(wf.groupId ? { groupId: wf.groupId } : {}),
|
|
328
|
+
...(wf.parallel ? { parallel: true } : {}),
|
|
310
329
|
kind: "flow",
|
|
311
330
|
},
|
|
312
331
|
});
|
|
@@ -317,17 +336,26 @@ export async function discoverTests(filePath) {
|
|
|
317
336
|
// contain ONLY contract.http(...). Stricter than MCP's gate: CLI
|
|
318
337
|
// emits flows as runnable tests via discoverTests, so silently
|
|
319
338
|
// dropping `contract.flow(...)` would hide an actual test. Any
|
|
320
|
-
// non-HTTP usage (including flow
|
|
321
|
-
// import error so the user
|
|
339
|
+
// non-HTTP usage (including flow, and a vNext workflow(...) — codex
|
|
340
|
+
// S2.6 R6 P2) → fail closed and surface the import error so the user
|
|
341
|
+
// knows discovery is degraded.
|
|
322
342
|
if (result.errors.length > 0) {
|
|
323
343
|
// Allow whitespace/newlines between `contract` and `.method` so the
|
|
324
344
|
// common fluent style `contract\n .flow(...)` still trips the gate.
|
|
325
345
|
const hasHttp = /contract\s*\.\s*http\b/i.test(content);
|
|
326
346
|
const hasNonHttp = /contract\s*\.\s*(?!http\b)\w+\s*[.(]/i.test(content);
|
|
327
|
-
|
|
347
|
+
// Import-clause check catches aliased workflow imports too
|
|
348
|
+
// (`import { workflow as wf }` — codex S2.6 R8 P2).
|
|
349
|
+
const hasWorkflow = /\bworkflow\s*\(/.test(content) ||
|
|
350
|
+
/import\s[^;]*?\{[^}]*\bworkflow\b[^}]*\}/.test(content);
|
|
351
|
+
const contracts = hasHttp && !hasNonHttp && !hasWorkflow ? extractContractCases(content) : [];
|
|
328
352
|
if (contracts.length > 0) {
|
|
329
353
|
for (const c of contracts) {
|
|
330
354
|
for (const caseItem of c.cases) {
|
|
355
|
+
// Static-fallback mirror of the runnable filter above (the AST
|
|
356
|
+
// extractor marks inboundCase()/direction literals).
|
|
357
|
+
if (caseItem.direction === "inbound")
|
|
358
|
+
continue;
|
|
331
359
|
const requires = caseItem.requires ?? "headless";
|
|
332
360
|
const defaultRun = caseItem.defaultRun ??
|
|
333
361
|
(requires !== "headless" ? "opt-in" : "always");
|
|
@@ -347,6 +375,7 @@ export async function discoverTests(filePath) {
|
|
|
347
375
|
requires: caseItem.requires,
|
|
348
376
|
defaultRun: caseItem.defaultRun,
|
|
349
377
|
deferred: caseItem.deferred,
|
|
378
|
+
deprecated: caseItem.deprecated,
|
|
350
379
|
kind: "contract",
|
|
351
380
|
},
|
|
352
381
|
});
|
|
@@ -395,7 +424,16 @@ export async function discoverTests(filePath) {
|
|
|
395
424
|
parallel: m.parallel,
|
|
396
425
|
requires: m.requires,
|
|
397
426
|
defaultRun: m.defaultRun,
|
|
398
|
-
|
|
427
|
+
// A vNext workflow is a graph orchestrator — it rides the "flow"
|
|
428
|
+
// RUNNABLE kind even when authored in a .test.ts, so the
|
|
429
|
+
// runnable-level suite filter and the --upload gate treat it like a
|
|
430
|
+
// flow (codex S2.6 R10 P2). NOTE: the FILE-level suite filter still
|
|
431
|
+
// maps .test.ts ↔ "test" (see resolveTestFilesForSuite's KNOWN
|
|
432
|
+
// LIMITATION) — a strict kinds:[flow] suite needs the workflow in a
|
|
433
|
+
// .flow.ts, or kinds:[test, flow].
|
|
434
|
+
kind: m.workflow ? "flow" : "test",
|
|
435
|
+
// WorkflowMeta.skip reason → deferred.
|
|
436
|
+
...(m.deferred !== undefined ? { deferred: m.deferred } : {}),
|
|
399
437
|
},
|
|
400
438
|
};
|
|
401
439
|
});
|
|
@@ -412,7 +450,6 @@ function matchesFilter(testItem, filter) {
|
|
|
412
450
|
export const __testing = {
|
|
413
451
|
matchesTags: (...args) => matchesTags(...args),
|
|
414
452
|
matchesExcludeTags: (...args) => matchesExcludeTags(...args),
|
|
415
|
-
flowStepsHaveBranch: (...args) => flowStepsHaveBranch(...args),
|
|
416
453
|
};
|
|
417
454
|
function matchesTags(testItem, tags, mode = "or") {
|
|
418
455
|
if (!testItem.meta.tags?.length)
|
|
@@ -506,7 +543,7 @@ export async function runCommand(target, options = {}) {
|
|
|
506
543
|
const targetDisplay = Array.isArray(target) ? target.join(", ") : target;
|
|
507
544
|
if (testFiles.length === 0) {
|
|
508
545
|
console.error(`\n${colors.red}❌ No test files found for target: ${Array.isArray(target) ? target.join(", ") : target}${colors.reset}`);
|
|
509
|
-
console.error(`${colors.dim}Glubean looks for files matching *.test.ts, *.contract.ts, or *.flow.ts in the target directory.${colors.reset}`);
|
|
546
|
+
console.error(`${colors.dim}Glubean looks for files matching *.test.ts, *.contract.ts, *.workflow.ts, or *.flow.ts in the target directory.${colors.reset}`);
|
|
510
547
|
console.error(`${colors.dim}Run "glubean run tests/" or "glubean run path/to/file.test.ts".${colors.reset}\n`);
|
|
511
548
|
await writeEmptyResult(target, runStartLocal);
|
|
512
549
|
process.exit(1);
|
|
@@ -759,48 +796,13 @@ export async function runCommand(target, options = {}) {
|
|
|
759
796
|
}
|
|
760
797
|
console.log(`${colors.dim}${parts.join(" + ")} (${testsToRun.length}/${totalDiscovered} tests)${colors.reset}`);
|
|
761
798
|
}
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
// (contract-flow-condition.md §12 / Spike 6.)
|
|
770
|
-
if (options.upload) {
|
|
771
|
-
// Exclude deferred (FlowMeta.skip → meta.deferred) flows: they don't
|
|
772
|
-
// execute — only a skipped row is uploaded — so their branches never reach
|
|
773
|
-
// Cloud and they must not block the upload.
|
|
774
|
-
const selectedFlows = testsToRun.filter((ft) => ft.test.meta.kind === "flow" && !ft.test.meta.deferred);
|
|
775
|
-
if (selectedFlows.length > 0) {
|
|
776
|
-
// Map each selected flow's source file → the set of its branch flow ids.
|
|
777
|
-
const branchIdsByFile = new Map();
|
|
778
|
-
for (const filePath of new Set(selectedFlows.map((ft) => ft.filePath))) {
|
|
779
|
-
try {
|
|
780
|
-
const extracted = await extractContractFromFile(filePath);
|
|
781
|
-
const ids = new Set();
|
|
782
|
-
for (const att of extracted.attachments ?? []) {
|
|
783
|
-
if (att.kind === "flow" && flowStepsHaveBranch(att.flow.steps))
|
|
784
|
-
ids.add(att.flow.id);
|
|
785
|
-
}
|
|
786
|
-
branchIdsByFile.set(filePath, ids);
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
// Real import/extraction errors are surfaced by discovery above.
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
const branchFlows = selectedFlows.filter((ft) => branchIdsByFile.get(ft.filePath)?.has(ft.test.meta.id));
|
|
793
|
-
if (branchFlows.length > 0) {
|
|
794
|
-
console.error(`${colors.red}Error: --upload does not yet support branch (condition/switch) flows.${colors.reset}`);
|
|
795
|
-
console.error(`${colors.dim}Glubean Cloud can't render these flows yet, and uploading would silently drop their branches:${colors.reset}`);
|
|
796
|
-
for (const ft of branchFlows) {
|
|
797
|
-
console.error(`${colors.dim} - ${ft.test.meta.id} (${ft.exportName}) [${relative(process.cwd(), ft.filePath)}]${colors.reset}`);
|
|
798
|
-
}
|
|
799
|
-
console.error(`${colors.dim}Run without --upload, or remove condition/switchOn/switchCond from these flows, until Cloud branch support lands.${colors.reset}`);
|
|
800
|
-
process.exit(1);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
799
|
+
// NOTE (0.6, owner): the run-view upload is UNCONDITIONAL — branch/poll
|
|
800
|
+
// workflows upload like any other. The earlier OPTION D gate refused them
|
|
801
|
+
// because Cloud "couldn't render" branch/poll run views, but refusing the
|
|
802
|
+
// upload is the wrong trade: uploading is always correct, and Cloud degrades
|
|
803
|
+
// gracefully (render raw + the projected `note`/`message` on opaque nodes)
|
|
804
|
+
// until a richer run view lands. The projection already uploads whole
|
|
805
|
+
// (branch/poll included), so the run view simply joins it.
|
|
804
806
|
console.log(`\n${colors.bold}Running ${testsToRun.length} test(s)...${colors.reset}\n`);
|
|
805
807
|
// ── Spike 3: runner input channels (attachment-model §8) ────────────────
|
|
806
808
|
// `--input-json` / `--bootstrap-json` / `--force-standalone` apply to a
|
|
@@ -1947,6 +1949,12 @@ export async function runCommand(target, options = {}) {
|
|
|
1947
1949
|
const built = await buildMetadata(scanResult, {
|
|
1948
1950
|
generatedBy: `@glubean/cli@${CLI_VERSION}`,
|
|
1949
1951
|
projectId,
|
|
1952
|
+
// Upload path only: carry the lossless full CONTRACT projection for
|
|
1953
|
+
// the Cloud c/f metadata snapshot. Deep-redacted below before upload;
|
|
1954
|
+
// never written to the on-disk metadata.json (that path omits it).
|
|
1955
|
+
// `workflows` is always present (Design Y) and redacted in the same
|
|
1956
|
+
// pass below.
|
|
1957
|
+
includeProjection: true,
|
|
1950
1958
|
});
|
|
1951
1959
|
metadata = built;
|
|
1952
1960
|
}
|
|
@@ -1974,6 +1982,20 @@ export async function runCommand(target, options = {}) {
|
|
|
1974
1982
|
}
|
|
1975
1983
|
metadata = { ...metadata, runPlan };
|
|
1976
1984
|
}
|
|
1985
|
+
// Deep-redact the FULL contract + workflow projection before upload. Test
|
|
1986
|
+
// events are redacted below via scope-based `redactEvent`, but the
|
|
1987
|
+
// projection is a free-form tree that can carry secrets anywhere
|
|
1988
|
+
// (examples, default headers, gRPC metadata, `extensions`/`meta`, literal
|
|
1989
|
+
// compare/switch values, assertion messages). `redactMetadataForUpload`
|
|
1990
|
+
// redacts ONLY the projection buckets (contractsProjection + workflows) —
|
|
1991
|
+
// never `files`/`rootHash` — so the server's test registry/dedup keeps
|
|
1992
|
+
// its verbatim sha256 hashes. The projection is uploaded WHOLE (branch/
|
|
1993
|
+
// poll included): it is the lossless source for the server snapshot, not
|
|
1994
|
+
// a run view (see the buildMetadata R14 note); the branch/poll run-view
|
|
1995
|
+
// gate is a separate layer, untouched here.
|
|
1996
|
+
if (metadata) {
|
|
1997
|
+
metadata = await redactMetadataForUpload(metadata, effectiveRedaction);
|
|
1998
|
+
}
|
|
1977
1999
|
const redactedPayload = {
|
|
1978
2000
|
...resultPayload,
|
|
1979
2001
|
metadata,
|