@glubean/cli 0.8.3 → 0.8.4

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.
@@ -0,0 +1,100 @@
1
+ export interface DryRunCommandOptions {
2
+ dir?: string;
3
+ json?: boolean;
4
+ out?: string;
5
+ }
6
+ /** One projected test, merged from static scan metadata + dynamic dry-run shape. */
7
+ export interface ProjectedTest {
8
+ testId: string;
9
+ exportName: string;
10
+ file: string;
11
+ description?: string;
12
+ deprecated?: string;
13
+ requires?: string;
14
+ defaultRun?: string;
15
+ tags?: string[];
16
+ assertions: Array<{
17
+ kind: string;
18
+ message?: string;
19
+ branch?: string;
20
+ }>;
21
+ endpoints: Array<{
22
+ method: string;
23
+ url: string;
24
+ branch?: string;
25
+ }>;
26
+ assertionCount: number;
27
+ projectionComplete: boolean;
28
+ incompleteReason?: string;
29
+ skipped?: boolean;
30
+ }
31
+ /** One projected contract (C1) — the scanner's static normalized contract, plus
32
+ * derived head fields, for upload to /projections/contract. */
33
+ export interface ProjectedContract {
34
+ contractId: string;
35
+ protocol: string;
36
+ target?: string;
37
+ description?: string;
38
+ deprecated?: string;
39
+ tags?: string[];
40
+ caseCount: number;
41
+ /** Full normalized contract (cases + schemas). */
42
+ projection: unknown;
43
+ projectionComplete: boolean;
44
+ incompleteReason?: string;
45
+ }
46
+ /** One projected workflow (C1) — the scanner's static normalized workflow. */
47
+ export interface ProjectedWorkflow {
48
+ workflowId: string;
49
+ name?: string;
50
+ description?: string;
51
+ tags?: string[];
52
+ nodeCount: number;
53
+ /** Full normalized workflow (node tree + phases). */
54
+ projection: unknown;
55
+ projectionComplete: boolean;
56
+ incompleteReason?: string;
57
+ }
58
+ export interface ProjectionResult {
59
+ projected: ProjectedTest[];
60
+ files: string[];
61
+ errors: Array<{
62
+ file: string;
63
+ message: string;
64
+ }>;
65
+ /** Scanner warnings (e.g. "Failed to extract metadata from <file>: …") — a
66
+ * file whose metadata couldn't be extracted is omitted from `files`/`projected`
67
+ * entirely, so a full-snapshot sync must treat these as fatal. */
68
+ warnings: string[];
69
+ /** Test-named files that yielded ZERO exports (parser recovered from a syntax
70
+ * error, or a misnamed module) — silently dropped, so also fatal for sync. */
71
+ emptyTestFiles: string[];
72
+ /** Statically-projected contracts (declarative — no dry-run). */
73
+ contracts: ProjectedContract[];
74
+ /** Statically-projected workflows (declarative — no dry-run). */
75
+ workflows: ProjectedWorkflow[];
76
+ /** OpenAPI 3.1 doc rendered from the project's HTTP contracts, for the Cloud
77
+ * api-reference view (endpoints/schemas with constraints/formats/enums). `null`
78
+ * when the project has no HTTP endpoints (a full sync then clears any stale doc).
79
+ * See `openapiFailed` for the render-error case. */
80
+ openapi?: Record<string, unknown> | null;
81
+ /** True when OpenAPI rendering THREW (bootstrap/extract/render). Distinct from a
82
+ * `null` doc: sync SKIPS the openapi upload on failure, so a transient error never
83
+ * wipes the project's previously-synced API reference. */
84
+ openapiFailed?: boolean;
85
+ }
86
+ /**
87
+ * Scan + dry-run a directory and merge static scan metadata
88
+ * (description/deprecated/requires/defaultRun/bareBranchCount) with each test's
89
+ * dynamic dry-run shape. Shared by `glubean dry-run` (print) and `glubean sync`
90
+ * (upload).
91
+ */
92
+ export declare function buildProjections(dir: string): Promise<ProjectionResult>;
93
+ /**
94
+ * `glubean dry-run` — project the SHAPE of every simple test (assertions made,
95
+ * endpoints hit) without running them, for cloud team review. Combines static
96
+ * scan metadata (description/deprecated/bareBranchCount) with the dynamic
97
+ * dry-run projection.
98
+ */
99
+ export declare function dryRunCommand(options?: DryRunCommandOptions): Promise<void>;
100
+ //# sourceMappingURL=dry-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dry-run.d.ts","sourceRoot":"","sources":["../../src/commands/dry-run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,237 @@
1
+ import { resolve, basename } from "node:path";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { scan, extractContractsFromProject } from "@glubean/scanner";
4
+ import { dryRunFiles, bootstrap } from "@glubean/runner";
5
+ import { renderArtifact, openapiArtifact, } from "@glubean/sdk";
6
+ const colors = {
7
+ reset: "\x1b[0m",
8
+ bold: "\x1b[1m",
9
+ dim: "\x1b[2m",
10
+ green: "\x1b[32m",
11
+ yellow: "\x1b[33m",
12
+ blue: "\x1b[34m",
13
+ red: "\x1b[31m",
14
+ };
15
+ /**
16
+ * Scan + dry-run a directory and merge static scan metadata
17
+ * (description/deprecated/requires/defaultRun/bareBranchCount) with each test's
18
+ * dynamic dry-run shape. Shared by `glubean dry-run` (print) and `glubean sync`
19
+ * (upload).
20
+ */
21
+ export async function buildProjections(dir) {
22
+ // Bootstrap the project's SDK runtime/plugins BEFORE any contract/flow module is
23
+ // imported — both `scan` (below) and `extractContractsFromProject` (for the OpenAPI
24
+ // render) import them. A project that relies on `glubean.setup.*` to make its
25
+ // contracts importable would otherwise cache a FAILED ESM evaluation from scan's
26
+ // first import, breaking every later extraction (the OpenAPI render would silently
27
+ // go null). Best-effort: a bootstrap failure must NOT break scan / dry-run for
28
+ // projects that don't need setup.
29
+ try {
30
+ await bootstrap(dir);
31
+ }
32
+ catch {
33
+ // ignore — scan/dry-run may still work; the OpenAPI extract fail-softs below
34
+ }
35
+ const scanResult = await scan(dir);
36
+ // Build a lookup of static metadata keyed by absolute file + export name.
37
+ const metaByKey = new Map();
38
+ const fileSet = new Set();
39
+ for (const [relPath, fileMeta] of Object.entries(scanResult.files)) {
40
+ const absPath = resolve(dir, relPath);
41
+ for (const exp of fileMeta.exports) {
42
+ // Only simple tests have a dry-runnable body; workflows are statically shaped.
43
+ if (exp.workflow)
44
+ continue;
45
+ fileSet.add(absPath);
46
+ metaByKey.set(`${absPath}::${exp.exportName}`, {
47
+ description: exp.description,
48
+ deprecated: exp.deprecated,
49
+ requires: exp.requires,
50
+ defaultRun: exp.defaultRun,
51
+ tags: exp.tags,
52
+ bareBranchCount: exp.bareBranchCount,
53
+ });
54
+ }
55
+ }
56
+ const files = [...fileSet];
57
+ const { shapes, errors } = await dryRunFiles(files, { cwd: dir });
58
+ const projected = shapes.map((s) => {
59
+ const meta = metaByKey.get(`${s.file}::${s.exportName}`) ?? {};
60
+ let projectionComplete = s.projectionComplete;
61
+ let incompleteReason = s.incompleteReason;
62
+ // Fold the static bare-branch signal: a native if/switch means only one arm
63
+ // was followed, so the shape is partial even if execution succeeded.
64
+ if (projectionComplete && meta.bareBranchCount && meta.bareBranchCount > 0) {
65
+ projectionComplete = false;
66
+ incompleteReason = `${meta.bareBranchCount} bare branch/loop(s) — use ctx.when()/ctx.switch()/ctx.while() for full projection`;
67
+ }
68
+ return {
69
+ testId: s.testId,
70
+ exportName: s.exportName,
71
+ file: s.file,
72
+ description: meta.description,
73
+ deprecated: meta.deprecated,
74
+ requires: meta.requires,
75
+ defaultRun: meta.defaultRun,
76
+ // Prefer the RUNTIME shape's tags (test.meta.tags) so data-driven `tagFields`
77
+ // row tags survive; fall back to the static scan tags when the project's
78
+ // runner predates shape tags (version skew), so statically-tagged tests
79
+ // aren't published with empty tags.
80
+ ...((s.tags ?? meta.tags)?.length ? { tags: s.tags ?? meta.tags } : {}),
81
+ assertions: s.assertions,
82
+ endpoints: s.endpoints,
83
+ assertionCount: s.assertionCount,
84
+ projectionComplete,
85
+ ...(incompleteReason ? { incompleteReason } : {}),
86
+ ...(s.skipped ? { skipped: true } : {}),
87
+ };
88
+ });
89
+ projected.sort((a, b) => a.testId.localeCompare(b.testId));
90
+ // Contracts + workflows are DECLARATIVE — the scanner already produced their full
91
+ // normalized projection statically (no dry-run). Map to the upload shape; the
92
+ // normalized blob is the reviewable body, the head fields derive from it.
93
+ const contracts = (scanResult.contractsProjection ?? []).map((c) => ({
94
+ contractId: c.id,
95
+ protocol: c.protocol,
96
+ target: c.target,
97
+ description: c.description,
98
+ deprecated: c.deprecated,
99
+ tags: c.tags,
100
+ caseCount: c.cases?.length ?? 0,
101
+ projection: c,
102
+ projectionComplete: !(c.unprojectableSchemas && c.unprojectableSchemas.length > 0),
103
+ ...(c.unprojectableSchemas && c.unprojectableSchemas.length > 0
104
+ ? { incompleteReason: `${c.unprojectableSchemas.length} declared schema(s) couldn't be projected` }
105
+ : {}),
106
+ }));
107
+ const workflows = (scanResult.workflows ?? []).map((w) => {
108
+ const g = w.gradeSummary ?? { full: 0, partial: 0, opaque: 0 };
109
+ const nodeCount = (g.full ?? 0) + (g.partial ?? 0) + (g.opaque ?? 0);
110
+ return {
111
+ workflowId: w.id,
112
+ name: w.name,
113
+ description: w.description,
114
+ tags: w.tags,
115
+ nodeCount,
116
+ projection: w,
117
+ projectionComplete: (g.opaque ?? 0) === 0,
118
+ ...((g.opaque ?? 0) > 0
119
+ ? { incompleteReason: `${g.opaque} node(s) projected opaque — not fully statically shaped` }
120
+ : {}),
121
+ };
122
+ });
123
+ contracts.sort((a, b) => a.contractId.localeCompare(b.contractId));
124
+ workflows.sort((a, b) => a.workflowId.localeCompare(b.workflowId));
125
+ // Render an OpenAPI 3.1 doc from the SAME contracts, so Cloud can show
126
+ // endpoints/schemas with the published api-reference view. This needs the
127
+ // RUNTIME extraction (real schema objects → JSON Schema); the static scan's
128
+ // `contractsProjection` can't produce it. Same-process ESM import is cached,
129
+ // so this doesn't re-run contract module side effects. Best-effort — a failure
130
+ // here never blocks the test/contract/workflow projection.
131
+ // Three DISTINCT states, because sync treats them differently:
132
+ // • a real doc (has HTTP paths) → upload it
133
+ // • no HTTP endpoints (null) → upload null, clearing any stale doc
134
+ // • render FAILED (openapiFailed) → sync SKIPS the upload, preserving the prior doc
135
+ // (so a transient bootstrap/extract/render error never wipes the API reference).
136
+ let openapi = null;
137
+ let openapiFailed = false;
138
+ try {
139
+ // bootstrap already ran at the top of buildProjections (before scan imported the
140
+ // contract modules), so the runtime extraction below resolves cleanly.
141
+ const { contracts: rawContracts, errors: extractErrors } = await extractContractsFromProject(dir);
142
+ if (extractErrors && extractErrors.length > 0) {
143
+ // Per-file import/synthesis failures are REPORTED here (not thrown). A partial
144
+ // extraction must not publish a half-doc or clear the prior one — skip + preserve.
145
+ openapiFailed = true;
146
+ }
147
+ else if (rawContracts.length > 0) {
148
+ const doc = renderArtifact(openapiArtifact, rawContracts, { title: basename(dir) || "API Specification" });
149
+ // An empty fallback (non-HTTP / inbound-only contracts) carries no paths — that's
150
+ // "no API reference" (null → clear), NOT a real doc.
151
+ const paths = doc?.paths;
152
+ openapi = paths && Object.keys(paths).length > 0 ? doc : null;
153
+ }
154
+ }
155
+ catch {
156
+ // Render threw — keep `openapi` null but flag the failure so sync skips the upload
157
+ // instead of clearing a previously-synced doc.
158
+ openapiFailed = true;
159
+ }
160
+ return {
161
+ projected,
162
+ files,
163
+ errors,
164
+ warnings: scanResult.warnings,
165
+ emptyTestFiles: scanResult.emptyTestFiles,
166
+ contracts,
167
+ workflows,
168
+ openapi,
169
+ openapiFailed,
170
+ };
171
+ }
172
+ /**
173
+ * `glubean dry-run` — project the SHAPE of every simple test (assertions made,
174
+ * endpoints hit) without running them, for cloud team review. Combines static
175
+ * scan metadata (description/deprecated/bareBranchCount) with the dynamic
176
+ * dry-run projection.
177
+ */
178
+ export async function dryRunCommand(options = {}) {
179
+ const dir = options.dir ? resolve(options.dir) : process.cwd();
180
+ const { projected, files, errors, warnings, emptyTestFiles, openapi } = await buildProjections(dir);
181
+ // Files that contribute NO projection though they look like tests — a sync
182
+ // would silently drop them (see syncCommand).
183
+ const dropped = [
184
+ ...warnings.filter((w) => w.startsWith("Failed to extract metadata from")),
185
+ ...emptyTestFiles.map((f) => `${f} — a Glubean test file with no extractable tests (syntax error, or tests removed/unrecognized?)`),
186
+ ];
187
+ if (options.out) {
188
+ await writeFile(resolve(options.out), JSON.stringify({ tests: projected, errors, warnings, emptyTestFiles, openapi }, null, 2));
189
+ }
190
+ if (options.json) {
191
+ console.log(JSON.stringify({ tests: projected, errors, warnings, emptyTestFiles, openapi }, null, 2));
192
+ return;
193
+ }
194
+ // ── Human-readable ──
195
+ console.log(`\n${colors.bold}${colors.blue}🔬 Glubean Dry-Run (shape projection)${colors.reset}\n`);
196
+ console.log(`${colors.dim}Directory: ${dir}${colors.reset}`);
197
+ console.log(`${colors.dim}Projected: ${projected.length} test(s) from ${files.length} file(s)${colors.reset}\n`);
198
+ for (const t of projected) {
199
+ const flag = t.skipped
200
+ ? `${colors.yellow}(skipped)${colors.reset}`
201
+ : t.projectionComplete
202
+ ? `${colors.green}● complete${colors.reset}`
203
+ : `${colors.yellow}◐ partial${colors.reset}`;
204
+ console.log(`${colors.bold}${t.testId}${colors.reset} ${flag}`);
205
+ if (t.description)
206
+ console.log(` ${colors.dim}${t.description}${colors.reset}`);
207
+ if (t.deprecated)
208
+ console.log(` ${colors.yellow}⚠ deprecated: ${t.deprecated}${colors.reset}`);
209
+ if (t.endpoints.length) {
210
+ const eps = t.endpoints.map((e) => `${e.method} ${e.url}`).join(", ");
211
+ console.log(` ${colors.dim}endpoints:${colors.reset} ${eps}`);
212
+ }
213
+ console.log(` ${colors.dim}assertions (${t.assertionCount}):${colors.reset}`);
214
+ for (const a of t.assertions) {
215
+ const label = a.message ? `${a.kind} "${a.message}"` : a.kind;
216
+ const branch = a.branch ? ` ${colors.dim}[${a.branch}]${colors.reset}` : "";
217
+ console.log(` - ${label}${branch}`);
218
+ }
219
+ if (!t.projectionComplete && t.incompleteReason) {
220
+ console.log(` ${colors.yellow}⚠ partial: ${t.incompleteReason}${colors.reset}`);
221
+ }
222
+ console.log();
223
+ }
224
+ if (errors.length) {
225
+ console.log(`${colors.red}Import errors:${colors.reset}`);
226
+ for (const e of errors)
227
+ console.log(` ${colors.red}✗ ${e.file}: ${e.message}${colors.reset}`);
228
+ console.log();
229
+ }
230
+ if (dropped.length) {
231
+ console.log(`${colors.red}Dropped files (their tests are omitted — sync would delete them):${colors.reset}`);
232
+ for (const w of dropped)
233
+ console.log(` ${colors.red}✗ ${w}${colors.reset}`);
234
+ console.log();
235
+ }
236
+ }
237
+ //# sourceMappingURL=dry-run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dry-run.js","sourceRoot":"","sources":["../../src/commands/dry-run.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,10 @@ interface RunOptions {
44
44
  includeOptIn?: boolean;
45
45
  upload?: boolean;
46
46
  uploadReceiptJson?: string;
47
+ /** ART1-B escape hatch — keep this run's local screenshot files after a
48
+ * successful `--upload` instead of the default post-upload cleanup
49
+ * (which unlinks only server-confirmed, whitelist-contained files). */
50
+ keepLocal?: boolean;
47
51
  project?: string;
48
52
  /**
49
53
  * Upload TARGET (the API/system under test the runs belong to). Resolved from
@@ -115,6 +119,20 @@ interface RunOptions {
115
119
  * per-profile gates that the legacy flat-shape path can't express.
116
120
  */
117
121
  thresholds?: import("@glubean/sdk").ThresholdConfig;
122
+ /**
123
+ * B2 M3 — `{id, rowIndex}` selector protocol.
124
+ *
125
+ * `onlyId` — run only the test(s) with a matching id (repeatable). For `.each`
126
+ * rows, pass the concrete expanded id (e.g. `user-0`).
127
+ *
128
+ * `row` — with a single `onlyId`, isolate the 0-based `.each` row index.
129
+ *
130
+ * `rerunFailed` — re-run only the tests that failed in the last `glubean run`
131
+ * (reads `.glubean/last-run.result.json`). Mutually exclusive with onlyId/row.
132
+ */
133
+ onlyId?: string[];
134
+ row?: number;
135
+ rerunFailed?: boolean;
118
136
  }
119
137
  export declare function findProjectConfig(startDir: string): Promise<{
120
138
  rootDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { bootstrap, evaluateThresholds, MetricCollector, ProjectRunner, buildRunContext, } from "@glubean/runner";
2
+ import { buildOnlySelectorsFromFlags, deriveRerunSelectors } from "../lib/only-selectors.js";
2
3
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { stat, readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
@@ -519,8 +520,8 @@ export async function runCommand(target, options = {}) {
519
520
  const interactive = capabilityProfile.browser;
520
521
  const traceCollector = [];
521
522
  console.log(`\n${colors.bold}${colors.blue}🧪 Glubean Test Runner${colors.reset}\n`);
522
- const testFiles = await resolveTestFiles(target);
523
- const isMultiFile = testFiles.length > 1;
523
+ let testFiles = await resolveTestFiles(target);
524
+ let isMultiFile = testFiles.length > 1;
524
525
  // Single string view of target for serialization / display paths
525
526
  // (result.json, junit, traces). Multi-suite passes an array; join with
526
527
  // ", " so downstream consumers still see a printable target field.
@@ -544,6 +545,59 @@ export async function runCommand(target, options = {}) {
544
545
  }
545
546
  const startDir = testFiles[0].substring(0, testFiles[0].lastIndexOf("/"));
546
547
  const { rootDir } = await findProjectConfig(startDir);
548
+ // ── B2 M3 — `{id, rowIndex}` "only" selectors ────────────────────────────
549
+ // Validate the --only-id / --row / --rerun-failed combo up front (single
550
+ // gate), then resolve the active selector set. `--rerun-failed` reads the
551
+ // previous run and narrows discovery to the files that failed; `--only-id` /
552
+ // `--row` narrow `testsToRun` below by template-matching concrete ids against
553
+ // static `.each` template ids. Either way the precise per-row filter runs in
554
+ // the harness subprocess via the GLUBEAN_RUNNER_ONLY_SELECTORS env channel.
555
+ let onlySelectors = [];
556
+ const selectorFlags = buildOnlySelectorsFromFlags({
557
+ onlyId: options.onlyId,
558
+ row: options.row,
559
+ rerunFailed: options.rerunFailed,
560
+ });
561
+ if (!selectorFlags.ok) {
562
+ console.error(`\n${colors.red}❌ ${selectorFlags.error}${colors.reset}\n`);
563
+ process.exit(1);
564
+ }
565
+ if (options.rerunFailed) {
566
+ const lastRunPath = resolve(rootDir, ".glubean", "last-run.result.json");
567
+ let lastRun;
568
+ try {
569
+ lastRun = JSON.parse(await readFile(lastRunPath, "utf-8"));
570
+ }
571
+ catch {
572
+ console.error(`\n${colors.red}❌ No previous run found. Run \`glubean run\` first.${colors.reset}\n` +
573
+ `${colors.dim}--rerun-failed reads ${lastRunPath}.${colors.reset}\n`);
574
+ process.exit(1);
575
+ }
576
+ const { selectors, files } = deriveRerunSelectors({ tests: lastRun.tests ?? [] });
577
+ if (selectors.length === 0) {
578
+ console.log(`\n${colors.green}✓ Last run had no failures — nothing to rerun.${colors.reset}\n`);
579
+ process.exit(0);
580
+ }
581
+ // Narrow discovery to the files that contained a failure. `files` were
582
+ // written relative to rootDir (resultPayload.tests.filePath) — rootDir is the
583
+ // stable basis (the dir holding .glubean/), so rerun resolves correctly even
584
+ // when invoked from a different cwd than the original run. Resolve both sides
585
+ // to absolute for an exact match against the discovered testFiles.
586
+ const failedFilesAbs = new Set(files.map((f) => resolve(rootDir, f)));
587
+ testFiles = testFiles.filter((f) => failedFilesAbs.has(resolve(f)));
588
+ isMultiFile = testFiles.length > 1;
589
+ if (testFiles.length === 0) {
590
+ console.error(`\n${colors.red}❌ --rerun-failed: none of the ${failedFilesAbs.size} failed file(s) ` +
591
+ `from the last run are in the current target.${colors.reset}\n`);
592
+ process.exit(1);
593
+ }
594
+ onlySelectors = selectors;
595
+ console.log(`${colors.dim}--rerun-failed: ${selectors.length} failed test(s) across ` +
596
+ `${testFiles.length} file(s)${colors.reset}\n`);
597
+ }
598
+ else {
599
+ onlySelectors = selectorFlags.selectors;
600
+ }
547
601
  // Config consolidation (docs/06 P2): the legacy package.json `glubean`
548
602
  // flat-shape is no longer read. Profile runs get run/redaction/thresholds
549
603
  // from the resolved plan (threaded via `options`); non-profile target runs
@@ -797,7 +851,7 @@ export async function runCommand(target, options = {}) {
797
851
  }
798
852
  const hasTags = options.tags && options.tags.length > 0;
799
853
  const hasExcludeTags = options.excludeTags && options.excludeTags.length > 0;
800
- const testsToRun = allFileTests.filter((ft) => {
854
+ let testsToRun = allFileTests.filter((ft) => {
801
855
  const tc = ft.test;
802
856
  if (tc.meta.skip)
803
857
  return false;
@@ -811,6 +865,26 @@ export async function runCommand(target, options = {}) {
811
865
  return false;
812
866
  return true;
813
867
  });
868
+ // B2 M3 — narrow testsToRun for --only-id / --row. Selector ids are CONCRETE
869
+ // (e.g. `user-0`); a `.each` export appears in discovery under its TEMPLATE id
870
+ // (e.g. `user-$index`). Template-match each selector id against the discovered
871
+ // template ids so concrete selectors reach the right export — the harness then
872
+ // applies the precise per-row filter at runtime. (Rerun narrows by file above.)
873
+ if (onlySelectors.length > 0 && !options.rerunFailed) {
874
+ const selectorIds = Array.from(new Set(onlySelectors.map((s) => (typeof s === "string" ? s : s.id))));
875
+ const indexed = testsToRun.map((ft) => ({ id: ft.test.meta.id, ft }));
876
+ const kept = new Set();
877
+ for (const selId of selectorIds) {
878
+ const match = findTemplateMatch(indexed, selId);
879
+ if (match)
880
+ kept.add(match.ft);
881
+ }
882
+ testsToRun = testsToRun.filter((ft) => kept.has(ft));
883
+ if (testsToRun.length === 0) {
884
+ console.error(`\n${colors.red}❌ No tests match --only-id ${selectorIds.join(", ")}${colors.reset}\n`);
885
+ process.exit(1);
886
+ }
887
+ }
814
888
  if (testsToRun.length === 0) {
815
889
  if (options.filter || hasTags) {
816
890
  const parts = [];
@@ -952,6 +1026,15 @@ export async function runCommand(target, options = {}) {
952
1026
  delete process.env["GLUBEAN_RUNNER_BOOTSTRAP_INPUT_MAP"];
953
1027
  delete process.env["GLUBEAN_RUNNER_FORCE_STANDALONE_IDS"];
954
1028
  }
1029
+ // B2 M3 — hand the resolved selector set to the harness subprocess (it applies
1030
+ // the precise per-row filter at runtime). Clear it otherwise so a stale value
1031
+ // never leaks across in-process invocations (parity with the input maps above).
1032
+ if (onlySelectors.length > 0) {
1033
+ process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"] = JSON.stringify(onlySelectors);
1034
+ }
1035
+ else {
1036
+ delete process.env["GLUBEAN_RUNNER_ONLY_SELECTORS"];
1037
+ }
955
1038
  if (options.pick) {
956
1039
  process.env.GLUBEAN_PICK = options.pick;
957
1040
  console.log(`${colors.dim} pick: ${options.pick}${colors.reset}`);
@@ -1024,6 +1107,7 @@ export async function runCommand(target, options = {}) {
1024
1107
  let currentTestItems;
1025
1108
  let testId = "";
1026
1109
  let testName = "";
1110
+ let testRowIndex = undefined;
1027
1111
  let testItem = null;
1028
1112
  let startTime = Date.now();
1029
1113
  let testEvents = [];
@@ -1089,6 +1173,7 @@ export async function runCommand(target, options = {}) {
1089
1173
  success: skippedClean ? true : finalSuccess,
1090
1174
  durationMs: duration,
1091
1175
  groupId: testItem?.meta.groupId,
1176
+ rowIndex: testRowIndex,
1092
1177
  });
1093
1178
  addLogEntry("result", skippedClean ? "SKIPPED" : finalSuccess ? "PASSED" : "FAILED", {
1094
1179
  duration,
@@ -1406,6 +1491,10 @@ export async function runCommand(target, options = {}) {
1406
1491
  (currentTestItems ? findFileTestByRuntimeId(currentTestItems, event.id) : undefined);
1407
1492
  testId = event.id;
1408
1493
  testName = entry?.test.meta.name || event.name || event.id;
1494
+ // rowIndex (B2 M3): the runtime start event is authoritative for the
1495
+ // per-row `.each` index (static discovery only sees the template id,
1496
+ // so it carries no rowIndex). undefined for non-each tests.
1497
+ testRowIndex = event.rowIndex;
1409
1498
  testItem = entry?.test || null;
1410
1499
  startTime = Date.now();
1411
1500
  testEvents = [];
@@ -1871,6 +1960,12 @@ export async function runCommand(target, options = {}) {
1871
1960
  success: r.success,
1872
1961
  durationMs: r.durationMs,
1873
1962
  events: r.events,
1963
+ // B2 M3 — persist rowIndex + filePath so `--rerun-failed` can reconstruct
1964
+ // the failed `{id, rowIndex}` selector set and narrow to the failed files.
1965
+ // filePath is relative to rootDir (the stable .glubean/ basis), NOT cwd, so
1966
+ // rerun resolves correctly when run from a different working directory.
1967
+ ...(r.rowIndex !== undefined && { rowIndex: r.rowIndex }),
1968
+ filePath: relative(rootDir, r.filePath),
1874
1969
  })),
1875
1970
  ...(thresholdSummary && { thresholds: thresholdSummary }),
1876
1971
  ...(options.meta && Object.keys(options.meta).length > 0 && { customMetadata: options.meta }),
@@ -1924,29 +2019,30 @@ export async function runCommand(target, options = {}) {
1924
2019
  }
1925
2020
  }
1926
2021
  // ── Screenshot paths ──────────────────────────────────────────────────
1927
- {
1928
- const screenshotPaths = [];
1929
- for (const run of collectedRuns) {
1930
- for (const event of run.events) {
1931
- if (event.type !== "event")
1932
- continue;
1933
- const ev = event.data;
1934
- if (ev.type === "browser:screenshot" && typeof ev.data?.path === "string") {
1935
- screenshotPaths.push(resolve(rootDir, ev.data.path));
1936
- }
2022
+ // This run's exact screenshot files, pulled from the `browser:screenshot`
2023
+ // event stream. Doubles as the upload whitelist below so `--upload` attaches
2024
+ // only THIS run's screenshots, not every file in the shared dir (ART1).
2025
+ const screenshotPaths = [];
2026
+ for (const run of collectedRuns) {
2027
+ for (const event of run.events) {
2028
+ if (event.type !== "event")
2029
+ continue;
2030
+ const ev = event.data;
2031
+ if (ev.type === "browser:screenshot" && typeof ev.data?.path === "string") {
2032
+ screenshotPaths.push(resolve(rootDir, ev.data.path));
1937
2033
  }
1938
2034
  }
1939
- if (screenshotPaths.length > 0) {
1940
- for (const p of screenshotPaths) {
1941
- console.log(`${colors.dim}Screenshot: ${colors.reset}${p}`);
1942
- }
1943
- console.log();
2035
+ }
2036
+ if (screenshotPaths.length > 0) {
2037
+ for (const p of screenshotPaths) {
2038
+ console.log(`${colors.dim}Screenshot: ${colors.reset}${p}`);
1944
2039
  }
2040
+ console.log();
1945
2041
  }
1946
2042
  // ── Cloud upload ────────────────────────────────────────────────────────
1947
2043
  if (options.upload) {
1948
2044
  const { resolveToken, resolveProjectId, resolveApiUrl } = await import("../lib/auth.js");
1949
- const { uploadToCloud } = await import("../lib/upload.js");
2045
+ const { uploadToCloud, removeUploadedScreenshots } = await import("../lib/upload.js");
1950
2046
  const authOpts = {
1951
2047
  token: options.token,
1952
2048
  project: options.project,
@@ -2096,7 +2192,27 @@ export async function runCommand(target, options = {}) {
2096
2192
  targetId,
2097
2193
  envFile: effectiveRun.envFile,
2098
2194
  rootDir,
2195
+ // Upload only THIS run's screenshots (whitelist), not the whole
2196
+ // shared `.glubean/screenshots` dir which accumulates prior runs.
2197
+ screenshotPaths,
2099
2198
  });
2199
+ // ART1-B — the shared screenshots dir only ever grows, so once the
2200
+ // Cloud confirmed it received this run's screenshots, delete the local
2201
+ // copies. Deletes ONLY uploadedFiles ∩ screenshotPaths (both server-
2202
+ // confirmed and provably this run's), realpath-contained to
2203
+ // `.glubean/screenshots`, and only while the on-disk file still has
2204
+ // its upload-time stat identity (re-checked just before each unlink)
2205
+ // — a failed/partial upload keeps its files, and a concurrently
2206
+ // recreated path is kept.
2207
+ if (!options.keepLocal &&
2208
+ uploadReceipt.artifactUpload.status === "uploaded" &&
2209
+ uploadReceipt.artifactUpload.uploadedFiles?.length &&
2210
+ screenshotPaths.length > 0) {
2211
+ const { removed } = await removeUploadedScreenshots(rootDir, screenshotPaths, uploadReceipt.artifactUpload.uploadedFiles);
2212
+ if (removed > 0) {
2213
+ console.log(`${colors.dim}Cleaned up ${removed} uploaded screenshot(s) from .glubean/screenshots (use --keep-local to keep them)${colors.reset}`);
2214
+ }
2215
+ }
2100
2216
  if (options.uploadReceiptJson) {
2101
2217
  const receiptPath = resolveOutputPath(options.uploadReceiptJson, process.cwd());
2102
2218
  await mkdir(dirname(receiptPath), { recursive: true });