@glubean/cli 0.5.0 → 0.8.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 -241
- package/dist/commands/contracts.js.map +1 -1
- package/dist/commands/load.d.ts +54 -0
- package/dist/commands/load.d.ts.map +1 -0
- package/dist/commands/load.js +270 -0
- package/dist/commands/load.js.map +1 -0
- package/dist/commands/migrate.js +3 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/run.d.ts +10 -27
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +111 -106
- 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/config.d.ts +2 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +3 -1
- package/dist/lib/config.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/main.d.ts.map +1 -1
- package/dist/main.js +11 -0
- package/dist/main.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,8 +7,9 @@ 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
|
-
import { extractContractFromFile, findTemplateMatch, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
|
|
12
|
+
import { buildSuffixes, classifyByStem, extractContractFromFile, findTemplateMatch, GLUBEAN_KINDS, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
|
|
12
13
|
import { applyEnvTemplating } from "@glubean/runner";
|
|
13
14
|
// ANSI color codes for pretty output
|
|
14
15
|
const colors = {
|
|
@@ -27,7 +28,7 @@ const CLOUD_MEMORY_LIMITS = {
|
|
|
27
28
|
pro: 700,
|
|
28
29
|
};
|
|
29
30
|
const MEMORY_WARNING_THRESHOLD_MB = CLOUD_MEMORY_LIMITS.free * 0.67;
|
|
30
|
-
async function findProjectConfig(startDir) {
|
|
31
|
+
export async function findProjectConfig(startDir) {
|
|
31
32
|
let dir = startDir;
|
|
32
33
|
while (dir !== "/") {
|
|
33
34
|
try {
|
|
@@ -49,29 +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) or
|
|
54
|
-
* a poll (bounded poll-until). Recurses into branch cases/default so a poll
|
|
55
|
-
* nested inside a branch body is caught too.
|
|
56
|
-
*
|
|
57
|
-
* Used to gate `--upload`: Glubean Cloud cannot render `kind:"branch"` or
|
|
58
|
-
* `kind:"poll"` flows yet, and uploading would silently drop them (local run view
|
|
59
|
-
* ≠ Cloud view), so we refuse rather than mislead. See contract-flow-condition.md
|
|
60
|
-
* §12 / contract-flow-poll.md §8.
|
|
61
|
-
*/
|
|
62
|
-
function flowStepsHaveBranchOrPoll(steps) {
|
|
63
|
-
if (!steps)
|
|
64
|
-
return false;
|
|
65
|
-
for (const s of steps) {
|
|
66
|
-
if (s.kind === "branch" || s.kind === "poll")
|
|
67
|
-
return true;
|
|
68
|
-
if (s.cases && s.cases.some((c) => flowStepsHaveBranchOrPoll(c.steps)))
|
|
69
|
-
return true;
|
|
70
|
-
if (s.default && flowStepsHaveBranchOrPoll(s.default))
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
53
|
// Config consolidation (docs/06): the package.json `glubean` field is no
|
|
76
54
|
// longer a config source. Warn (don't error) when one lingers so users
|
|
77
55
|
// migrate it into glubean.yaml instead of wondering why it stopped working.
|
|
@@ -99,15 +77,27 @@ function isGlob(target) {
|
|
|
99
77
|
// calls execute and register overlays before discovery runs (attachment-
|
|
100
78
|
// model §7.4). `discoverTests()` is responsible for distinguishing
|
|
101
79
|
// bootstrap-only files from runnable-emitting ones.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
];
|
|
80
|
+
// CLI target resolution for `glubean run` recognizes only `.ts` files across the
|
|
81
|
+
// test-runner kinds (including bootstrap), derived from the canonical kind
|
|
82
|
+
// registry (scanner/kinds.ts). `load` is EXCLUDED here: load plans run through
|
|
83
|
+
// the dedicated `glubean load` command + the closed-model orchestrator, not the
|
|
84
|
+
// per-test ProjectRunner, so `glubean run ./dir` must not sweep in `.load.ts`.
|
|
85
|
+
const TEST_FILE_SUFFIXES = GLUBEAN_KINDS.filter((k) => k.kind !== "load").flatMap((k) => buildSuffixes(k.stems, [".ts"]));
|
|
108
86
|
function isGlubeanTestFile(name) {
|
|
109
87
|
return TEST_FILE_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
110
88
|
}
|
|
89
|
+
// Files that carry contract/workflow DECLARATIONS and are safe to
|
|
90
|
+
// runtime-import for extraction. Matched by SUFFIX, not substring: a normal
|
|
91
|
+
// test like `checkout.workflow.test.ts` ends in `.test.ts` and must stay on
|
|
92
|
+
// the static test path — a `.workflow.` substring would mis-route it into
|
|
93
|
+
// contract extraction (which ignores `test()` exports), silently dropping its
|
|
94
|
+
// tests (codex 0.6 P2).
|
|
95
|
+
// Contract/workflow/flow artifact files (NOT bootstrap), all extensions —
|
|
96
|
+
// derived from the canonical kind registry (scanner/kinds.ts).
|
|
97
|
+
const RUNTIME_ARTIFACT_SUFFIXES = GLUBEAN_KINDS.filter((k) => k.runtimeArtifact && k.kind !== "bootstrap").flatMap((k) => buildSuffixes(k.stems));
|
|
98
|
+
function isRuntimeExtractableArtifact(name) {
|
|
99
|
+
return RUNTIME_ARTIFACT_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
100
|
+
}
|
|
111
101
|
function isBootstrapOnlyFile(name) {
|
|
112
102
|
return name.endsWith(".bootstrap.ts");
|
|
113
103
|
}
|
|
@@ -126,15 +116,8 @@ async function walkTestFiles(dir, result) {
|
|
|
126
116
|
}
|
|
127
117
|
}
|
|
128
118
|
export function classifyGlubeanFile(filePath) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (filePath.endsWith(".contract.ts"))
|
|
132
|
-
return "contract";
|
|
133
|
-
if (filePath.endsWith(".flow.ts"))
|
|
134
|
-
return "flow";
|
|
135
|
-
if (filePath.endsWith(".bootstrap.ts"))
|
|
136
|
-
return "bootstrap";
|
|
137
|
-
return undefined;
|
|
119
|
+
// `.ts`-only classification, by stem, from the canonical kind registry.
|
|
120
|
+
return classifyByStem(filePath, [".ts"]);
|
|
138
121
|
}
|
|
139
122
|
async function resolveSingleTarget(target) {
|
|
140
123
|
const abs = resolve(target);
|
|
@@ -230,6 +213,14 @@ export async function resolveTestFilesForSuite(target, kinds) {
|
|
|
230
213
|
// `.flow.ts` file (recommended canonical layout) or declare the
|
|
231
214
|
// suite as `kinds: [contract, flow]` so both candidate file types
|
|
232
215
|
// are scanned and the runnable-level filter sorts them out.
|
|
216
|
+
//
|
|
217
|
+
// The same applies to a vNext workflow authored in a `.test.ts`
|
|
218
|
+
// (workflows ride the "flow" RUNNABLE kind, codex S2.6 R10/R11): a
|
|
219
|
+
// strict `kinds: [flow]` suite won't see the file. Move the workflow
|
|
220
|
+
// to a `.flow.ts` (recommended — it also gains the metadata
|
|
221
|
+
// projection, which `.test.ts` files never get) or declare
|
|
222
|
+
// `kinds: [test, flow]`; the runnable-level filter then keeps the
|
|
223
|
+
// workflow and drops the plain test() exports.
|
|
233
224
|
return files.filter((f) => {
|
|
234
225
|
const k = classifyGlubeanFile(f);
|
|
235
226
|
if (k === undefined)
|
|
@@ -251,16 +242,21 @@ export async function discoverTests(filePath) {
|
|
|
251
242
|
return [];
|
|
252
243
|
}
|
|
253
244
|
const content = await readFile(filePath, "utf-8");
|
|
254
|
-
if (
|
|
245
|
+
if (isRuntimeExtractableArtifact(filePath)) {
|
|
255
246
|
// Runtime extraction via shared function (supports .with() syntax).
|
|
256
|
-
// Returns BOTH contracts and
|
|
257
|
-
//
|
|
258
|
-
// per contract case.
|
|
247
|
+
// Returns BOTH contracts and workflows; `.workflow.ts` / `.flow.ts` files
|
|
248
|
+
// often export only workflows, so we must emit one DiscoveredTest per
|
|
249
|
+
// workflow in addition to per contract case.
|
|
259
250
|
const result = await extractContractFromFile(filePath);
|
|
260
251
|
const results = [];
|
|
261
252
|
for (const ec of result.contracts) {
|
|
262
253
|
const contractTags = ec.tags ?? [];
|
|
263
254
|
for (const c of ec.cases) {
|
|
255
|
+
// Non-runnable cases (direction: "inbound", design §9.5): the SDK
|
|
256
|
+
// registered no Test — advertising them here would schedule an id
|
|
257
|
+
// that can never resolve (codex I2 R1 P1).
|
|
258
|
+
if (c.runnable === false)
|
|
259
|
+
continue;
|
|
264
260
|
// Mirror SDK dispatchContract: finalTags = contract + case + runtime
|
|
265
261
|
// synthetic. Without this, pre-spawn excludeTags / --tag filtering
|
|
266
262
|
// skips contract cases entirely (Phase 1 filter reads meta.tags).
|
|
@@ -292,22 +288,28 @@ export async function discoverTests(filePath) {
|
|
|
292
288
|
});
|
|
293
289
|
}
|
|
294
290
|
}
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
291
|
+
// vNext workflows (S2.6): each BuiltWorkflow wraps the graph in ONE
|
|
292
|
+
// simple test — discover it like a flow orchestrator, so a file whose
|
|
293
|
+
// projection scan/upload advertises is also runnable (codex S2.6 R2 P2).
|
|
294
|
+
// During the migration window workflows ride the "flow" runnable kind:
|
|
295
|
+
// a workflow IS the vNext orchestrator replacing contract.flow(), and
|
|
296
|
+
// suites declaring `kinds: [flow]` mean "run the graph orchestrators in
|
|
297
|
+
// these files" — no new user-facing kinds enum until flow is deleted.
|
|
298
|
+
// WorkflowMeta.skip → deferred mirrors the SDK's own Test wrapping.
|
|
299
|
+
for (const wf of result.workflows) {
|
|
303
300
|
results.push({
|
|
304
|
-
exportName:
|
|
301
|
+
exportName: wf.exportName,
|
|
305
302
|
meta: {
|
|
306
|
-
id:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
id: wf.id,
|
|
304
|
+
name: wf.name,
|
|
305
|
+
description: wf.description,
|
|
306
|
+
tags: wf.tags,
|
|
307
|
+
only: wf.only,
|
|
308
|
+
deferred: wf.skip,
|
|
309
|
+
// data-driven members: grouping + concurrency ride the projection
|
|
310
|
+
// (codex S2.12 R1 P2 — the registry alone never reaches the CLI).
|
|
311
|
+
...(wf.groupId ? { groupId: wf.groupId } : {}),
|
|
312
|
+
...(wf.parallel ? { parallel: true } : {}),
|
|
311
313
|
kind: "flow",
|
|
312
314
|
},
|
|
313
315
|
});
|
|
@@ -318,17 +320,26 @@ export async function discoverTests(filePath) {
|
|
|
318
320
|
// contain ONLY contract.http(...). Stricter than MCP's gate: CLI
|
|
319
321
|
// emits flows as runnable tests via discoverTests, so silently
|
|
320
322
|
// dropping `contract.flow(...)` would hide an actual test. Any
|
|
321
|
-
// non-HTTP usage (including flow
|
|
322
|
-
// import error so the user
|
|
323
|
+
// non-HTTP usage (including flow, and a vNext workflow(...) — codex
|
|
324
|
+
// S2.6 R6 P2) → fail closed and surface the import error so the user
|
|
325
|
+
// knows discovery is degraded.
|
|
323
326
|
if (result.errors.length > 0) {
|
|
324
327
|
// Allow whitespace/newlines between `contract` and `.method` so the
|
|
325
328
|
// common fluent style `contract\n .flow(...)` still trips the gate.
|
|
326
329
|
const hasHttp = /contract\s*\.\s*http\b/i.test(content);
|
|
327
330
|
const hasNonHttp = /contract\s*\.\s*(?!http\b)\w+\s*[.(]/i.test(content);
|
|
328
|
-
|
|
331
|
+
// Import-clause check catches aliased workflow imports too
|
|
332
|
+
// (`import { workflow as wf }` — codex S2.6 R8 P2).
|
|
333
|
+
const hasWorkflow = /\bworkflow\s*\(/.test(content) ||
|
|
334
|
+
/import\s[^;]*?\{[^}]*\bworkflow\b[^}]*\}/.test(content);
|
|
335
|
+
const contracts = hasHttp && !hasNonHttp && !hasWorkflow ? extractContractCases(content) : [];
|
|
329
336
|
if (contracts.length > 0) {
|
|
330
337
|
for (const c of contracts) {
|
|
331
338
|
for (const caseItem of c.cases) {
|
|
339
|
+
// Static-fallback mirror of the runnable filter above (the AST
|
|
340
|
+
// extractor marks inboundCase()/direction literals).
|
|
341
|
+
if (caseItem.direction === "inbound")
|
|
342
|
+
continue;
|
|
332
343
|
const requires = caseItem.requires ?? "headless";
|
|
333
344
|
const defaultRun = caseItem.defaultRun ??
|
|
334
345
|
(requires !== "headless" ? "opt-in" : "always");
|
|
@@ -397,7 +408,16 @@ export async function discoverTests(filePath) {
|
|
|
397
408
|
parallel: m.parallel,
|
|
398
409
|
requires: m.requires,
|
|
399
410
|
defaultRun: m.defaultRun,
|
|
400
|
-
|
|
411
|
+
// A vNext workflow is a graph orchestrator — it rides the "flow"
|
|
412
|
+
// RUNNABLE kind even when authored in a .test.ts, so the
|
|
413
|
+
// runnable-level suite filter and the --upload gate treat it like a
|
|
414
|
+
// flow (codex S2.6 R10 P2). NOTE: the FILE-level suite filter still
|
|
415
|
+
// maps .test.ts ↔ "test" (see resolveTestFilesForSuite's KNOWN
|
|
416
|
+
// LIMITATION) — a strict kinds:[flow] suite needs the workflow in a
|
|
417
|
+
// .flow.ts, or kinds:[test, flow].
|
|
418
|
+
kind: m.workflow ? "flow" : "test",
|
|
419
|
+
// WorkflowMeta.skip reason → deferred.
|
|
420
|
+
...(m.deferred !== undefined ? { deferred: m.deferred } : {}),
|
|
401
421
|
},
|
|
402
422
|
};
|
|
403
423
|
});
|
|
@@ -414,7 +434,7 @@ function matchesFilter(testItem, filter) {
|
|
|
414
434
|
export const __testing = {
|
|
415
435
|
matchesTags: (...args) => matchesTags(...args),
|
|
416
436
|
matchesExcludeTags: (...args) => matchesExcludeTags(...args),
|
|
417
|
-
|
|
437
|
+
isGlubeanTestFile: (name) => isGlubeanTestFile(name),
|
|
418
438
|
};
|
|
419
439
|
function matchesTags(testItem, tags, mode = "or") {
|
|
420
440
|
if (!testItem.meta.tags?.length)
|
|
@@ -508,7 +528,7 @@ export async function runCommand(target, options = {}) {
|
|
|
508
528
|
const targetDisplay = Array.isArray(target) ? target.join(", ") : target;
|
|
509
529
|
if (testFiles.length === 0) {
|
|
510
530
|
console.error(`\n${colors.red}❌ No test files found for target: ${Array.isArray(target) ? target.join(", ") : target}${colors.reset}`);
|
|
511
|
-
console.error(`${colors.dim}Glubean looks for files matching *.test.ts, *.contract.ts, or *.flow.ts in the target directory.${colors.reset}`);
|
|
531
|
+
console.error(`${colors.dim}Glubean looks for files matching *.test.ts, *.contract.ts, *.workflow.ts, or *.flow.ts in the target directory.${colors.reset}`);
|
|
512
532
|
console.error(`${colors.dim}Run "glubean run tests/" or "glubean run path/to/file.test.ts".${colors.reset}\n`);
|
|
513
533
|
await writeEmptyResult(target, runStartLocal);
|
|
514
534
|
process.exit(1);
|
|
@@ -761,48 +781,13 @@ export async function runCommand(target, options = {}) {
|
|
|
761
781
|
}
|
|
762
782
|
console.log(`${colors.dim}${parts.join(" + ")} (${testsToRun.length}/${totalDiscovered} tests)${colors.reset}`);
|
|
763
783
|
}
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
// (contract-flow-condition.md §12 / Spike 6.)
|
|
772
|
-
if (options.upload) {
|
|
773
|
-
// Exclude deferred (FlowMeta.skip → meta.deferred) flows: they don't
|
|
774
|
-
// execute — only a skipped row is uploaded — so their branches never reach
|
|
775
|
-
// Cloud and they must not block the upload.
|
|
776
|
-
const selectedFlows = testsToRun.filter((ft) => ft.test.meta.kind === "flow" && !ft.test.meta.deferred);
|
|
777
|
-
if (selectedFlows.length > 0) {
|
|
778
|
-
// Map each selected flow's source file → the set of its branch/poll flow ids.
|
|
779
|
-
const branchIdsByFile = new Map();
|
|
780
|
-
for (const filePath of new Set(selectedFlows.map((ft) => ft.filePath))) {
|
|
781
|
-
try {
|
|
782
|
-
const extracted = await extractContractFromFile(filePath);
|
|
783
|
-
const ids = new Set();
|
|
784
|
-
for (const att of extracted.attachments ?? []) {
|
|
785
|
-
if (att.kind === "flow" && flowStepsHaveBranchOrPoll(att.flow.steps))
|
|
786
|
-
ids.add(att.flow.id);
|
|
787
|
-
}
|
|
788
|
-
branchIdsByFile.set(filePath, ids);
|
|
789
|
-
}
|
|
790
|
-
catch {
|
|
791
|
-
// Real import/extraction errors are surfaced by discovery above.
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
const branchFlows = selectedFlows.filter((ft) => branchIdsByFile.get(ft.filePath)?.has(ft.test.meta.id));
|
|
795
|
-
if (branchFlows.length > 0) {
|
|
796
|
-
console.error(`${colors.red}Error: --upload does not yet support branch (condition/switch) or poll flows.${colors.reset}`);
|
|
797
|
-
console.error(`${colors.dim}Glubean Cloud can't render these flows yet, and uploading would silently drop their branches/polls:${colors.reset}`);
|
|
798
|
-
for (const ft of branchFlows) {
|
|
799
|
-
console.error(`${colors.dim} - ${ft.test.meta.id} (${ft.exportName}) [${relative(process.cwd(), ft.filePath)}]${colors.reset}`);
|
|
800
|
-
}
|
|
801
|
-
console.error(`${colors.dim}Run without --upload, or remove condition/switchOn/switchCond/poll from these flows, until Cloud support lands.${colors.reset}`);
|
|
802
|
-
process.exit(1);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
784
|
+
// NOTE (0.6, owner): the run-view upload is UNCONDITIONAL — branch/poll
|
|
785
|
+
// workflows upload like any other. The earlier OPTION D gate refused them
|
|
786
|
+
// because Cloud "couldn't render" branch/poll run views, but refusing the
|
|
787
|
+
// upload is the wrong trade: uploading is always correct, and Cloud degrades
|
|
788
|
+
// gracefully (render raw + the projected `note`/`message` on opaque nodes)
|
|
789
|
+
// until a richer run view lands. The projection already uploads whole
|
|
790
|
+
// (branch/poll included), so the run view simply joins it.
|
|
806
791
|
console.log(`\n${colors.bold}Running ${testsToRun.length} test(s)...${colors.reset}\n`);
|
|
807
792
|
// ── Spike 3: runner input channels (attachment-model §8) ────────────────
|
|
808
793
|
// `--input-json` / `--bootstrap-json` / `--force-standalone` apply to a
|
|
@@ -1949,6 +1934,12 @@ export async function runCommand(target, options = {}) {
|
|
|
1949
1934
|
const built = await buildMetadata(scanResult, {
|
|
1950
1935
|
generatedBy: `@glubean/cli@${CLI_VERSION}`,
|
|
1951
1936
|
projectId,
|
|
1937
|
+
// Upload path only: carry the lossless full CONTRACT projection for
|
|
1938
|
+
// the Cloud c/f metadata snapshot. Deep-redacted below before upload;
|
|
1939
|
+
// never written to the on-disk metadata.json (that path omits it).
|
|
1940
|
+
// `workflows` is always present (Design Y) and redacted in the same
|
|
1941
|
+
// pass below.
|
|
1942
|
+
includeProjection: true,
|
|
1952
1943
|
});
|
|
1953
1944
|
metadata = built;
|
|
1954
1945
|
}
|
|
@@ -1976,6 +1967,20 @@ export async function runCommand(target, options = {}) {
|
|
|
1976
1967
|
}
|
|
1977
1968
|
metadata = { ...metadata, runPlan };
|
|
1978
1969
|
}
|
|
1970
|
+
// Deep-redact the FULL contract + workflow projection before upload. Test
|
|
1971
|
+
// events are redacted below via scope-based `redactEvent`, but the
|
|
1972
|
+
// projection is a free-form tree that can carry secrets anywhere
|
|
1973
|
+
// (examples, default headers, gRPC metadata, `extensions`/`meta`, literal
|
|
1974
|
+
// compare/switch values, assertion messages). `redactMetadataForUpload`
|
|
1975
|
+
// redacts ONLY the projection buckets (contractsProjection + workflows) —
|
|
1976
|
+
// never `files`/`rootHash` — so the server's test registry/dedup keeps
|
|
1977
|
+
// its verbatim sha256 hashes. The projection is uploaded WHOLE (branch/
|
|
1978
|
+
// poll included): it is the lossless source for the server snapshot, not
|
|
1979
|
+
// a run view (see the buildMetadata R14 note); the branch/poll run-view
|
|
1980
|
+
// gate is a separate layer, untouched here.
|
|
1981
|
+
if (metadata) {
|
|
1982
|
+
metadata = await redactMetadataForUpload(metadata, effectiveRedaction);
|
|
1983
|
+
}
|
|
1979
1984
|
const redactedPayload = {
|
|
1980
1985
|
...resultPayload,
|
|
1981
1986
|
metadata,
|