Files
jackwener__opencli/src/validate.test.ts
jakevin 9cae777430 chore(release): pre-release P0/P1 cleanup (#1393)
* chore(release): pre-release P0/P1 cleanup

P0 fixes:
- delete src/analysis.ts (179 lines, 0 importers across src/clis/extension)
- remove dead OPENCLI_DIAGNOSTIC negative test assertion
- rename OPENCLI_BROWSER_TIMEOUT to OPENCLI_BROWSER_IDLE_TIMEOUT — the env
  controls workspace lease idle release, not command runtime; old name was
  misleading and undocumented (no fallback needed)
- add 'fill' to validate.ts KNOWN_STEP_NAMES so adapters using PR #1222's
  fill pipeline step do not trip "unknown step name" warnings during validate

P1 fixes:
- BrowserConnect daemon-not-running hint: replace stale "make sure port is
  available" with actionable "run opencli doctor / opencli daemon restart"
- TimeoutError hint: lead with --timeout flag, demote env var to secondary

* fix(validate): derive step allowlist from pipeline registry

@pr-monitor flagged the prior "add 'fill' to KNOWN_STEP_NAMES" fix as
treating only the symptom — two parallel hand-maintained lists will keep
drifting whenever a new pipeline step is registered.

Address the root cause: pipeline/registry.ts now exports
`getRegisteredStepNames()` and validate.ts builds KNOWN_STEP_NAMES from
that. Adding a step via `registerStep()` automatically allowlists it.

* test(validate): regression guard for pipeline step allowlist linkage

@pr-monitor follow-up: lock the validate ↔ pipeline registry linkage at
the test layer so future drift is caught immediately.

Changes:
- recompute KNOWN_STEP_NAMES per-call (was const at module load) so
  steps registered after validate.ts import (plugins, dynamic registration)
  are honoured
- add src/validate.test.ts with 3 cases:
  1. every step name from getRegisteredStepNames() exists
  2. an adapter using every currently registered step does not warn
  3. a step registered at runtime is automatically allowlisted by
     validate without any source change to validate.ts

* fix(capabilityRouting): add fill to BROWSER_ONLY_STEPS

Same double-list drift pattern as validate.ts KNOWN_STEP_NAMES (audit
follow-up flagged in this PR's evolution thread). The fill step was
registered in pipeline/registry.ts (PR #1222) but never added to the
browser-only allowlist in capabilityRouting.ts.

Concrete impact:
- shouldUseBrowserSession() didn't recognize a `[{ fill: ... }]` pipeline
  as needing a browser, so PUBLIC adapters using fill could end up
  without a page and crash inside stepFill at `page!.fillText(...)`
- pipeline/executor.ts's per-step retry policy (BROWSER_ONLY_STEPS gets
  2 retries on transient errors, others get 0) skipped fill — losing
  retry coverage on a DOM-touching step

Fix:
- add 'fill' to BROWSER_ONLY_STEPS
- add a documenting comment explaining BROWSER_ONLY_STEPS is the
  browser-touching subset of registered steps (not the full set)
- export _validateBrowserOnlyStepsAgainstRegistry() so the test layer
  catches the inverse drift (browser-only step that no longer exists)
- 3 new tests in capabilityRouting.test.ts:
  * pipeline with fill routes to browser session
  * BROWSER_ONLY_STEPS subset of registered step names
  * fill is in both lists

This addresses @pr-monitor follow-up #3 (audit similar double-list
patterns) for the obvious in-scope candidate. Other candidates outside
this PR's scope: build-manifest serialization vs registry shape, error
code unions vs lint baselines.

* test(validate): use Strategy.PUBLIC enum instead of string cast in regression test

Self-review nit: `strategy: 'public' as never` worked but bypassed the
typed CliOptions union. Use `Strategy.PUBLIC` so the test exercises the
real public API.
2026-05-07 21:11:34 +08:00

95 lines
3.5 KiB
TypeScript

/**
* Tests for src/validate.ts.
*
* Focus: regression guards for the "single source of truth" link between
* pipeline step registry (src/pipeline/registry.ts) and validate.ts step
* allowlist. A new step registered via `registerStep()` must automatically
* be allowlisted by `opencli validate` — no parallel hand-maintained list.
*/
import { describe, it, expect } from 'vitest';
import { getRegisteredStepNames, registerStep } from './pipeline/registry.js';
import { cli, getRegistry, Strategy } from './registry.js';
import { validateClisWithTarget } from './validate.js';
describe('validate.ts pipeline step allowlist', () => {
it('uses every step name registered in pipeline/registry.ts', () => {
const registered = getRegisteredStepNames();
expect(registered).toContain('navigate');
expect(registered).toContain('click');
expect(registered).toContain('type');
expect(registered).toContain('fill');
expect(registered).toContain('fetch');
expect(registered.length).toBeGreaterThanOrEqual(15);
});
it('does not warn for any step name currently registered in the pipeline registry', () => {
// Snapshot the registry before mutating it for the test.
const reg = getRegistry();
const original = reg.get('validate-allowlist-test/all-steps');
if (original) reg.delete('validate-allowlist-test/all-steps');
const allRegisteredSteps = getRegisteredStepNames();
cli({
site: 'validate-allowlist-test',
name: 'all-steps',
access: 'read',
browser: false,
strategy: Strategy.PUBLIC,
args: [],
pipeline: allRegisteredSteps.map(stepName => ({ [stepName]: {} })),
func: async () => [],
});
try {
const report = validateClisWithTarget([], 'validate-allowlist-test/all-steps');
const r = report.results[0];
const unknownStepWarning = r.warnings.find(w => w.startsWith('Pipeline step '));
expect(unknownStepWarning).toBeUndefined();
} finally {
reg.delete('validate-allowlist-test/all-steps');
if (original) reg.set('validate-allowlist-test/all-steps', original);
}
});
it('newly registered step automatically appears in validator allowlist', () => {
const customStep = '__test_custom_step__';
expect(getRegisteredStepNames()).not.toContain(customStep);
registerStep(customStep, async (_p, _params, data) => data);
try {
expect(getRegisteredStepNames()).toContain(customStep);
const reg = getRegistry();
const original = reg.get('validate-dynamic-test/uses-custom');
if (original) reg.delete('validate-dynamic-test/uses-custom');
cli({
site: 'validate-dynamic-test',
name: 'uses-custom',
access: 'read',
browser: false,
strategy: Strategy.PUBLIC,
args: [],
pipeline: [{ [customStep]: {} }],
func: async () => [],
});
try {
const report = validateClisWithTarget([], 'validate-dynamic-test/uses-custom');
const r = report.results[0];
const unknownStepWarning = r.warnings.find(w => w.includes(customStep));
expect(unknownStepWarning).toBeUndefined();
} finally {
reg.delete('validate-dynamic-test/uses-custom');
if (original) reg.set('validate-dynamic-test/uses-custom', original);
}
} finally {
// Best-effort cleanup of the test step. There is no `unregisterStep` —
// leaving it registered is harmless because the test step name is
// namespaced and never used outside this file.
}
});
});