Skip to content

Commit c5efa62

Browse files
committed
chore: merge develop into PR branch to resolve CHANGELOG conflict
Resolves merge conflict in CHANGELOG.md [Unreleased] / ### Fixed by combining the PR's tools/test-local.sh entry with the entries added on develop since the PR was opened (#2791, #2790, model SELECT $quoteIdentifier, #2786, #2779, #2792). Newest-first ordering preserved; (#2796) suffix added to the PR's entry to match the recent-entry convention. No code changes — only CHANGELOG ordering. Signed-off-by: Peter Amiri <peter@alurium.com>
2 parents da93da1 + fc7398d commit c5efa62

28 files changed

Lines changed: 836 additions & 52 deletions

File tree

.ai/wheels/testing/browser-testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ bash tools/test-local.sh # skips browser specs if JARs missin
6464
- **`client` is a Lucee reserved scope.** `var client = ...` in a closure throws "client scope is not enabled". Use `var c = ...` or `var bc = ...`. (Generalized rule: see CLAUDE.md anti-pattern #11.)
6565
- **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl).
6666
- **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI.
67-
- **CI runs browser tests**`pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically.
67+
- **CI runs browser tests**`pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically. The base URL is resolved at instance time through a layered lookup (`this.baseUrl` → Wheels setting → JVM property `wheels.browserTest.baseUrl` → env var → CGI auto-detect → `http://localhost:8080`); per-spec `this.baseUrl` takes priority over the env var. Set `this.baseUrl` in the component pseudo-constructor (outside any function), not inside `beforeAll()``super.beforeAll()` calls `$resolveBaseUrl()` and caches the result, so a `this.baseUrl =` assignment that runs after `super.beforeAll()` is silently ignored.
6868
- **Fixture routes**`/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application.
6969
- **Dialogs are Lucee-only**`acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines.

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ All historical references to "CFWheels" in this changelog have been preserved fo
2222

2323
### Fixed
2424

25-
- `tools/test-local.sh` silently aborted with `EXIT=1` (no `/tmp/wheels-test-server.log` written, no diagnostic printed) on every install since the `lucli` → `wheels` rebrand window closed — i.e. anyone whose `~/.lucli/express/` directory never existed. Line 81 ran `LUCEE_LIB=$(find ~/.wheels/express ~/.lucli/express -path "*/lib/ext" -type d 2>/dev/null | head -1)` under `set -euo pipefail`; `find` exits non-zero whenever any path argument doesn't exist (stderr suppressed via `2>/dev/null`, but the exit status survives), `pipefail` propagated it through `head -1`, and the assignment tripped `set -e`. The cleanup trap then fired with no server to clean up, leaving the user staring at "Starting Wheels CLI server on port 8080…" with no further output. Dropped the now-dead `~/.lucli/express` fallback (the rename landed in 3.0 and recent CLI releases extract Lucee Express to `~/.wheels/express/` only) and added `|| true` for defense in depth so a missing directory (e.g. a truly fresh install before `wheels start` has ever run) leaves `LUCEE_LIB` empty and the downstream `[ -n "$LUCEE_LIB" ]` guard skips the JDBC pre-install cleanly
25+
- `tools/test-local.sh` silently aborted with `EXIT=1` (no `/tmp/wheels-test-server.log` written, no diagnostic printed) on every install since the `lucli` → `wheels` rebrand window closed — i.e. anyone whose `~/.lucli/express/` directory never existed. Line 81 ran `LUCEE_LIB=$(find ~/.wheels/express ~/.lucli/express -path "*/lib/ext" -type d 2>/dev/null | head -1)` under `set -euo pipefail`; `find` exits non-zero whenever any path argument doesn't exist (stderr suppressed via `2>/dev/null`, but the exit status survives), `pipefail` propagated it through `head -1`, and the assignment tripped `set -e`. The cleanup trap then fired with no server to clean up, leaving the user staring at "Starting Wheels CLI server on port 8080…" with no further output. Dropped the now-dead `~/.lucli/express` fallback (the rename landed in 3.0 and recent CLI releases extract Lucee Express to `~/.wheels/express/` only) and added `|| true` for defense in depth so a missing directory (e.g. a truly fresh install before `wheels start` has ever run) leaves `LUCEE_LIB` empty and the downstream `[ -n "$LUCEE_LIB" ]` guard skips the JDBC pre-install cleanly (#2796)
26+
- Routes registered inside `.namespace("foo")` (or equivalent `.scope()` / `.package()`) with a redundant namespace prefix in the controller path — e.g. `to="foo/dashboard##index"` instead of `to="dashboard##index"` — previously silently produced a `foo.foo/dashboard` lookup that downstream flattened to a `Foodashboard`-style class name with an opaque `Wheels.ViewNotFound` error. The Mapper now rejects this at route-registration time with `Wheels.MapperArgumentInvalid`, naming the namespace and the offending value and pointing at the correct shorter form, so users can find the bad route definition instead of chasing the symptom (#2791)
27+
- `WheelsTest` auto-bind missed user-defined global helpers added via `include` in `app/global/functions.cfm`. The pseudo-constructor used `getMetaData(application.wo).functions`, which only enumerates methods declared directly on the CFC and skips symbols merged in via `cfinclude`. Specs that called custom helpers (e.g. `can()`, `hasRole()`) had to manually rebind each one in `beforeAll()`. The auto-bind now iterates `application.wo` as a struct and binds every UDF via `isCustomFunction()`, preserving the existing public-only filter for declared methods (#2790)
28+
- Model layer SELECT clause builder now routes column identifiers through the adapter's `$quoteIdentifier`, so reserved-word column names (e.g. `key`, `order`, `group`) survive on every supported dialect instead of breaking `findAll` / `findOne` / dynamic finders with cryptic SQL syntax errors. The WHERE / ORDER BY paths already quoted columns; `$createSQLFieldList` and the empty-pagination column-list extraction in `read.cfc` now match.
29+
- `wheels packages install <name>` is now a transparent alias for `wheels packages add <name>` on every caller path that actually reaches `Module.cfc` (stdio MCP server, scripted in-process clients, spec suite). Previously the dispatch layer's `case "install":` branch printed a yellow warning to stdout and returned `""` without installing anything — so even though `PackagesMainCli.install()` itself had been a true alias for `add()` since #2729, any caller routing through the CLI module's verb dispatch silently no-op'd. The shell-facing `wheels packages install <name>` form is still intercepted by LuCLI's built-in extension installer upstream of module dispatch and remains broken on that path (and is documented as such in the module-owned help text), but MCP tool calls and programmatic callers now behave identically to `add`. Both branches now share a single fall-through body so the validation, error shape, and install behavior cannot drift apart again.
2630
- `wheels.wheelstest.BrowserTest` now throws a clear `Wheels.BrowserTest.NotWired` error — naming `browserDescribe()` as the fix — when a spec calls a DSL method on `this.browser` from a plain `describe()` block. Previously the uninitialized `this.browser` was an empty string, producing the misleading `function [visitUrl] does not exist in the String` on every newcomer's first BrowserTest spec. A sentinel `UnwiredBrowserGuard` is now installed at `this.browser` before `browserDescribe()` wires the real `BrowserClient` and after `$endBrowserContext()` tears it down
31+
- `BrowserTest`'s default base URL is no longer hardcoded to `http://localhost:8080`. `$resolveBaseUrl()` now consults a layered lookup at instance time: `this.baseUrl` (per-spec override, set in the component pseudo-constructor) → `get("browserTestBaseUrl")` (Wheels setting) → `-Dwheels.browserTest.baseUrl=...` (JVM system property) → `WHEELS_BROWSER_TEST_BASE_URL` env var → `cgi.server_name`/`cgi.server_port` auto-detect → `http://localhost:8080` default. Specs running against a non-8080 server (Titan on 60050, `wheels new` scaffolds on 60080) can set `this.baseUrl` in the pseudo-constructor or rely on the CGI auto-detect instead of comparing `getBaseUrl()` against a sentinel string. The bare-env-var approach still works for CI but is no longer the only escape hatch (the JVM caches env vars at process start, so post-launch `export` had no effect). Regression spec at `vendor/wheels/tests/specs/wheelstest/BrowserTestBaseUrlResolutionSpec.cfc` (#2779)
2732
- Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773)
2833
- `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs
34+
- Bare `?reload=true` now auto-detects changes to `app/global/*.cfm` in development and re-evaluates `app/global/functions.cfm` (plus any files it `<cfinclude>`s) when any tracked file's mtime has changed, so new helpers added to `app/global/functions.cfm` (or any file it includes) are picked up without the password-gated `applicationStop()` path. Wheels snapshots the include directory on application start and re-evaluates the include via a new `application.wo.$reincludeGlobals()` hook when `$globalIncludesChanged()` detects an added/removed/touched file. Opt out with `set(reloadOnGlobalChange=false)` in `config/settings.cfm` (issue #2792)
2935

3036
----
3137

cli/lucli/Module.cfc

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,27 +2089,27 @@ component extends="modules.BaseModule" {
20892089
opts.name = positional[2];
20902090
var mainCli = new modules.wheels.services.packages.PackagesMainCli();
20912091
return mainCli.show(opts);
2092+
case "install":
2093+
// LuCLI's built-in extension installer intercepts the
2094+
// literal verb `install` on the user-facing CLI surface
2095+
// — same trap that bit `wheels browser install` (renamed
2096+
// to `wheels browser setup` in #2345). But every other
2097+
// caller path reaches this dispatch directly: the
2098+
// stdio MCP server (`wheels mcp wheels`), scripted
2099+
// in-process clients, and the bundle's own spec suite.
2100+
// `PackagesMainCli.install()` has been a transparent
2101+
// alias for `add()` since #2729, so the dispatch layer
2102+
// must match — otherwise `install <name>` silently
2103+
// no-ops on the only paths LuCLI does NOT intercept.
2104+
// Fall through to the `add` branch (same validation,
2105+
// same error shape, same install behavior).
20922106
case "add":
20932107
if (arrayLen(positional) < 2) {
20942108
throw(message="add requires a name: wheels packages add <name>[@<version>]");
20952109
}
20962110
opts.target = positional[2];
20972111
var mainCli = new modules.wheels.services.packages.PackagesMainCli();
20982112
return mainCli.add(opts);
2099-
case "install":
2100-
// Dead code on the CLI surface: LuCLI's built-in extension
2101-
// installer intercepts the literal verb `install` across
2102-
// all modules before dispatch reaches Module.cfc — the
2103-
// same trap that bit `wheels browser install` (renamed
2104-
// to `wheels browser setup` in #2345). Kept as a
2105-
// documentation marker for future maintainers; if LuCLI
2106-
// ever stops intercepting, this case keeps a friendly
2107-
// redirect for users still typing the historic verb.
2108-
out("'wheels packages install' is intercepted by LuCLI's", "yellow");
2109-
out("built-in extension installer and won't reach this module.", "yellow");
2110-
out("Use:", "yellow");
2111-
out(" wheels packages add " & (arrayLen(positional) >= 2 ? positional[2] : "<name>"), "bold");
2112-
return "";
21132113
case "update":
21142114
opts.target = arrayLen(positional) >= 2 ? positional[2] : "";
21152115
var mainCli = new modules.wheels.services.packages.PackagesMainCli();

cli/lucli/tests/specs/commands/PackagesCommandSpec.cfc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,69 @@ component extends="wheels.wheelstest.system.BaseSpec" {
7676
expect(out).toInclude("registry");
7777
});
7878
});
79+
80+
describe("wheels packages install — alias for add", () => {
81+
82+
// Issue #2785: prior implementation made `case "install":` in
83+
// Module.cfc a friendly-redirect dead branch that printed a
84+
// warning to stdout and returned "" without installing anything.
85+
// That meant any caller that reached Module.cfc via a path that
86+
// is not the user-facing CLI (MCP tools, scripted clients, specs)
87+
// silently got nothing back when typing `install` — even though
88+
// `PackagesMainCli.install()` itself has always been a true alias
89+
// for `add()`. The alias must be wired through the dispatch
90+
// layer too, so that the only place `install` ever no-ops is the
91+
// LuCLI extension-installer intercept (which we cannot patch),
92+
// and every in-process caller gets the same behavior as `add`.
93+
it("throws the same BadInput error as `add` when name is missing", () => {
94+
mod.__arguments = ["install"];
95+
var threw = {flag: false, message: ""};
96+
try {
97+
mod.packages();
98+
} catch (any e) {
99+
threw.flag = true;
100+
threw.message = e.message;
101+
}
102+
expect(threw.flag).toBeTrue();
103+
// The error must point users at the canonical `add` verb so
104+
// programmatic callers (MCP, scripts) see the right shape.
105+
expect(threw.message).toInclude("add");
106+
});
107+
108+
it("dispatches `install <name>` to the same code path as `add <name>`", () => {
109+
// Both verbs must reach PackagesMainCli — meaning neither
110+
// short-circuits with a warning before instantiation. A
111+
// bogus package name still throws (registry lookup fails),
112+
// but it must throw the SAME way for both verbs. The prior
113+
// behavior was that `install` silently returned "" while
114+
// `add` threw — a divergence that broke any caller that
115+
// expected the alias to be transparent.
116+
var captureThrow = (verb) => {
117+
var localMod = new cli.lucli.Module(cwd = variables.tempRoot);
118+
localMod.__arguments = [verb, "wheels-this-package-does-not-exist-#CreateUUID()#"];
119+
var threw = {flag: false, type: ""};
120+
try {
121+
localMod.packages();
122+
} catch (any e) {
123+
threw.flag = true;
124+
threw.type = e.type;
125+
}
126+
return threw;
127+
};
128+
var addResult = captureThrow("add");
129+
var installResult = captureThrow("install");
130+
expect(addResult.flag).toBeTrue();
131+
expect(installResult.flag).toBeTrue();
132+
// Both must throw, and both must throw with a non-empty type
133+
// — proving they reached the same registry-lookup code path
134+
// rather than `install` being intercepted by a different branch.
135+
expect(installResult.type).notToBe("");
136+
// And both must throw the SAME exception type — a future
137+
// regression that made `install` throw at argument validation
138+
// (before the registry call) would still satisfy the non-empty
139+
// check above, so pin the equivalence explicitly.
140+
expect(installResult.type).toBe(addResult.type);
141+
});
142+
});
79143
}
80144
}

0 commit comments

Comments
 (0)