From 1680be0d18912325114701551167a3dcbc7c86e6 Mon Sep 17 00:00:00 2001 From: Shizuku <2163018547@qq.com> Date: Wed, 30 Sep 2026 08:10:11 +0800 Subject: [PATCH 1/5] fix(web): 404 single-segment paths that are not registered locales Stray single-segment paths that contain a dot (/foo.txt, /llms-full.txt) skip the middleware's locale redirect by design, then bind `[locale]` to that segment and render the shared home page as a 200 with `lang="foo.txt"` - a soft 404 for crawlers and generative-engine probes. The locale layout rejects any locale outside the routed registry with notFound(), so those URLs answer a real 404 (noindex, branded not-found page), while every registered locale, static asset, and route handler keeps its current behavior. --- web/app/[locale]/layout.tsx | 10 ++++++++- web/lib/i18n/locale-layout-guard.test.ts | 27 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 web/lib/i18n/locale-layout-guard.test.ts diff --git a/web/app/[locale]/layout.tsx b/web/app/[locale]/layout.tsx index 3a69af7b1a..6e14eb877b 100644 --- a/web/app/[locale]/layout.tsx +++ b/web/app/[locale]/layout.tsx @@ -1,10 +1,11 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; +import { notFound } from "next/navigation"; import { Nav } from "@/components/nav"; import { Footer } from "@/components/footer"; import { UsageCounting } from "@/components/usage-counting"; import { BUILD_FACTS } from "@/lib/facts"; -import { localeDirection, locales, type Locale } from "@/lib/i18n/config"; +import { isValidLocale, localeDirection, locales, type Locale } from "@/lib/i18n/config"; import { getChrome, getHome } from "@/lib/i18n/dictionaries"; import { serializeJsonLd } from "@/lib/json-ld"; import { buildPageMetadata } from "@/lib/page-meta"; @@ -74,6 +75,13 @@ export default async function LocaleLayout({ params: Promise<{ locale: string }>; }) { const { locale } = await params; + // A single-segment URL that is not a routed locale — e.g. a stray dotted + // path such as /foo.txt, which middleware deliberately leaves alone so real + // static files keep resolving — would otherwise bind `[locale]` to that + // segment and render the shared home page as a 200 under a fake locale + // (`lang="foo.txt"`). An unregistered locale is not a page: answer with an + // honest 404 and let the not-found boundary render instead. + if (!isValidLocale(locale)) notFound(); const chrome = getChrome(locale); // RTL locales (e.g. ar) set the document direction from the canonical // registry so the browser handles bidirectional layout from the root. diff --git a/web/lib/i18n/locale-layout-guard.test.ts b/web/lib/i18n/locale-layout-guard.test.ts new file mode 100644 index 0000000000..4593726c30 --- /dev/null +++ b/web/lib/i18n/locale-layout-guard.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +/** + * A single-segment URL that is not a routed locale — a stray dotted path such + * as /foo.txt, which middleware deliberately leaves alone so real static files + * keep resolving — binds `[locale]` to that segment and would otherwise render + * the shared home page as a 200 under a fake locale (`lang="foo.txt"`): a + * soft 404 for crawlers and generative-engine probes. The layout must turn any + * locale outside the routed registry into a real 404 before it renders. + */ +describe("locale layout rejects unregistered locales", () => { + const layout = readFileSync(new URL("../../app/[locale]/layout.tsx", import.meta.url), "utf8"); + + it("guards on isValidLocale and calls notFound()", () => { + expect(layout).toContain("import { isValidLocale, localeDirection, locales, type Locale }"); + expect(layout).toContain("if (!isValidLocale(locale)) notFound();"); + }); + + it("checks the locale before reading dictionaries or rendering chrome", () => { + const guard = layout.indexOf("isValidLocale(locale)"); + const chrome = layout.indexOf("getChrome(locale)"); + expect(guard).toBeGreaterThan(-1); + expect(chrome).toBeGreaterThan(-1); + expect(guard).toBeLessThan(chrome); + }); +}); From 8d98ec8601e50a3abe31bcb95062b527ca080f1d Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 21:25:40 -0700 Subject: [PATCH 2/5] test(web): exercise unknown-locale rejection through the layout Preserve PR #6786 and its contributor commits while consolidating its source-string assertions into the existing behavior suite. Cover six invalid segments before dictionary access, noindex metadata without home canonical links, every registered locale and text direction, and the default-locale not-found shell. Keep the dotted-path explanation at the existing production guard. Focused locale-layout/registry/middleware tests: 56 passed, 0 failed. Removing both locale guards produces 12 intended failures and 19 passes; exact source restored afterward. Full npm and check:web gates pending at this checkpoint. --- web/app/[locale]/layout.tsx | 10 +++----- web/lib/i18n/locale-layout-guard.test.ts | 27 -------------------- web/lib/locale-layout.test.ts | 32 ++++++++++++++++-------- 3 files changed, 25 insertions(+), 44 deletions(-) delete mode 100644 web/lib/i18n/locale-layout-guard.test.ts diff --git a/web/app/[locale]/layout.tsx b/web/app/[locale]/layout.tsx index 54dc4f019f..6c684b426c 100644 --- a/web/app/[locale]/layout.tsx +++ b/web/app/[locale]/layout.tsx @@ -80,12 +80,10 @@ export default async function LocaleLayout({ params: Promise<{ locale: string }>; }) { const { locale } = await params; - // A single-segment URL that is not a routed locale — e.g. a stray dotted - // path such as /foo.txt, which middleware deliberately leaves alone so real - // static files keep resolving — would otherwise bind `[locale]` to that - // segment and render the shared home page as a 200 under a fake locale - // (`lang="foo.txt"`; likewise `/wp-login.php`). An unregistered locale is not a page: answer with an - // honest 404 and let the not-found boundary render instead. + // Dotted paths bypass locale redirection so real files keep resolving. + // An unknown path such as /foo.txt (or /foo.txt/faq) still binds `[locale]` + // here. Reject it before reading dictionaries or rendering home chrome, + // so nonexistent files never become HTTP 200 pages with a fake html lang. if (!isValidLocale(locale)) notFound(); const chrome = getChrome(locale); // RTL locales (e.g. ar) set the document direction from the canonical diff --git a/web/lib/i18n/locale-layout-guard.test.ts b/web/lib/i18n/locale-layout-guard.test.ts deleted file mode 100644 index 70114599bd..0000000000 --- a/web/lib/i18n/locale-layout-guard.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; - -/** - * A single-segment URL that is not a routed locale — a stray dotted path such - * as /foo.txt, which middleware deliberately leaves alone so real static files - * keep resolving — binds `[locale]` to that segment and would otherwise render - * the shared home page as a 200 under a fake locale (`lang="foo.txt"`): a - * soft 404 for crawlers and generative-engine probes. The layout must turn any - * locale outside the routed registry into a real 404 before it renders. - */ -describe("locale layout rejects unregistered locales", () => { - const layout = readFileSync(new URL("../../app/[locale]/layout.tsx", import.meta.url), "utf8"); - - it("guards on isValidLocale and calls notFound()", () => { - expect(layout).toMatch(/import \{[^}]*\bisValidLocale\b[^}]*\} from "@\/lib\/i18n\/config"/); - expect(layout).toContain("if (!isValidLocale(locale)) notFound();"); - }); - - it("checks the locale before reading dictionaries or rendering chrome", () => { - const guard = layout.indexOf("isValidLocale(locale)"); - const chrome = layout.indexOf("getChrome(locale)"); - expect(guard).toBeGreaterThan(-1); - expect(chrome).toBeGreaterThan(-1); - expect(guard).toBeLessThan(chrome); - }); -}); diff --git a/web/lib/locale-layout.test.ts b/web/lib/locale-layout.test.ts index 3b04ec9855..2db4c8c18e 100644 --- a/web/lib/locale-layout.test.ts +++ b/web/lib/locale-layout.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { localeDirection, locales } from "./i18n/config"; +import * as dictionaries from "./i18n/dictionaries"; vi.mock("next/font/local", () => ({ default: () => ({ variable: "font" }) })); @@ -9,12 +11,16 @@ function render(locale: string) { return LocaleLayout({ children: null, params: Promise.resolve({ locale }) }); } +afterEach(() => vi.restoreAllMocks()); + describe("locale layout", () => { - // Middleware skips dotted paths, so `/wp-login.php` reaches `[locale]`. - it("answers not-found for a segment that is not a locale", async () => { - await expect(render("wp-login.php")).rejects.toMatchObject({ - digest: expect.stringContaining("404"), - }); + // Middleware leaves dotted files alone; unknown files reach `[locale]` + // just like unknown directory segments, including their child routes. + const invalidLocales = ["foo.txt", "llms-full.txt", "robots.json", "wp-login.php", "xx", ""]; + it.each(invalidLocales)("rejects %j before reading chrome dictionaries", async (locale) => { + const chrome = vi.spyOn(dictionaries, "getChrome"); + await expect(render(locale)).rejects.toMatchObject({ digest: "NEXT_HTTP_ERROR_FALLBACK;404" }); + expect(chrome).not.toHaveBeenCalled(); }); // The layout cannot catch its own notFound(), so the root boundary answers. @@ -29,14 +35,18 @@ describe("locale layout", () => { }); // Otherwise the home page's title, canonical, and hreflang stream into it. - it("gives that not-found the not-found metadata, not the home page's", async () => { - const metadata = await generateMetadata({ params: Promise.resolve({ locale: "wp-login.php" }) }); + it.each(invalidLocales)("gives %j noindex metadata without a home dictionary", async (locale) => { + const home = vi.spyOn(dictionaries, "getHome"); + const metadata = await generateMetadata({ params: Promise.resolve({ locale }) }); expect(metadata.title).toBe("Not found · Codewhale"); + expect(metadata.robots).toEqual({ index: false, follow: true }); expect(metadata.alternates).toEqual({}); + expect(home).not.toHaveBeenCalled(); }); - it("renders a real locale", async () => { - const element = await render("en"); - expect(element.props.lang).toBe("en"); + it.each(locales)("renders the registered locale %s", async (locale) => { + const element = await render(locale); + expect(element.props.lang).toBe(locale); + expect(element.props.dir).toBe(localeDirection(locale)); }); }); From 8c2baf42a83686c6e0b321130a4ad84c07a2439b Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 20:04:34 -0700 Subject: [PATCH 3/5] test(config): use the supported concise verbosity in save fixtures Six typed-save fixtures used quiet even though the validated enum accepts normal, concise and verbose. Use concise for these writes; preserve the separate raw legacy-load coverage. Six focused config tests passed, zero failed. Shared source gate: npm test674 passed/0 failed; check:web passed. No production configuration behavior changed. Signed-off-by: Hunter B --- crates/config/src/tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index be6b750e11..da1ebea38f 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -10281,7 +10281,7 @@ fn typed_save_round_trips_every_builtin_provider_selector() { }; store .config - .set_value("verbosity", "quiet") + .set_value("verbosity", "concise") .expect("set verbosity"); store.save().expect("typed save"); let reloaded = ConfigStore::load(Some(path.clone())) @@ -10299,7 +10299,7 @@ fn legacy_siliconflow_cn_spelling_loads_and_is_repaired_on_save() { assert_eq!(store.config.provider, ProviderKind::SiliconflowCN); store .config - .set_value("verbosity", "quiet") + .set_value("verbosity", "concise") .expect("set verbosity"); store.save().expect("typed save"); let body = fs::read_to_string(&path).expect("read config"); @@ -10318,7 +10318,7 @@ fn typed_save_keeps_a_providers_section_holding_only_a_legacy_kind_table() { }; store .config - .set_value("verbosity", "quiet") + .set_value("verbosity", "concise") .expect("set verbosity"); store.save().expect("typed save"); let body = fs::read_to_string(&path).expect("read config"); @@ -10342,7 +10342,7 @@ fn typed_save_keeps_runtime_owned_keys_in_typed_sub_tables() { let mut store = ConfigStore::load(Some(path.clone())).expect("load config"); store .config - .set_value("verbosity", "quiet") + .set_value("verbosity", "concise") .expect("set verbosity"); store.save().expect("typed save"); let saved: toml::Table = toml::from_str(&fs::read_to_string(&path).expect("read config")) From 68ddb6d71ce4c02120dd7f68153c81e854867427 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 21:06:38 -0700 Subject: [PATCH 4/5] test: align portable config and plugin host fixtures with current contracts Portable imports reject machine-bound authority settings. Keep closed-choice acceptance coverage on portable verbosity and preserve unknown existing fields. Run ordinary typed plugin activation under normal supervision rather than the 600 ms fault watchdog, and use the clippy-approved search error assertion. Validation on combined source 69824d325 plus recorded patch: CLI bundle tests 61 passed, 0 failed; typed plugin activation 1 passed; custom search fallback 1 passed. Combined all-feature CLI/TUI/workflow/workflow-js test build passed. Unrelated Calm glyph assertion remains open (TUI combined 97 passed, 1 failed). Full npm test: 740 passed, 0 failed (68 wrapper + 16 SDK + 54 extension + 602 web); check:web passed. Windows handshake scheduling remains separate, unverified. Signed-off-by: Hunter B --- crates/cli/src/config_bundles.rs | 23 +++++++++++++---------- crates/tui/src/extension_host/tests.rs | 5 ++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/config_bundles.rs b/crates/cli/src/config_bundles.rs index 8ab266156e..00765811f3 100644 --- a/crates/cli/src/config_bundles.rs +++ b/crates/cli/src/config_bundles.rs @@ -2699,7 +2699,7 @@ log_level = "trace" bundle .preferences .entries - .insert("allow_shell".into(), toml::Value::Boolean(false)); + .insert("log_level".into(), toml::Value::String("info".into())); let error = apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path()) .expect_err("closed choice must fail before the transaction"); assert!(error.to_string().contains(key), "{error:#}"); @@ -2721,16 +2721,18 @@ log_level = "trace" #[test] fn closed_choice_imports_accept_reader_values_without_revalidating_old_fields() { - for (key, value) in [ - ("approval_policy", " On-Request "), - ("sandbox_mode", " WORKSPACE-WRITE "), - ("verbosity", " CONCISE "), - ] { + // Trust posture is intentionally machine-bound and rejected by the + // bundle boundary even when its value is otherwise valid. Exercise + // the portable closed choice here; authority rejection has its own + // no-write and export-scrubbing regressions below. + for value in [" CONCISE ", "normal"] { + let key = "verbosity"; let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.toml"); // Loading and unrelated round trips still preserve legacy text; only // the value being written gets the shared closed-choice validation. - let original = "# Preserve old fields\nverbosity = 'quiet'\n"; + let original = + "# Preserve old fields\napproval_policy = 'legacy-unknown'\nverbosity = 'quiet'\n"; std::fs::write(&path, original).unwrap(); let mut store = ConfigStore::load(Some(path.clone())).unwrap(); assert_eq!(std::fs::read_to_string(&path).unwrap(), original); @@ -2749,9 +2751,10 @@ log_level = "trace" ); let reloaded = ConfigStore::load(Some(path)).unwrap(); assert_eq!(reloaded.config.get_value(key).as_deref(), Some(value)); - if key != "verbosity" { - assert_eq!(reloaded.config.verbosity.as_deref(), Some("quiet")); - } + assert_eq!( + reloaded.config.approval_policy.as_deref(), + Some("legacy-unknown") + ); } } diff --git a/crates/tui/src/extension_host/tests.rs b/crates/tui/src/extension_host/tests.rs index f0b111c158..d495e3a13c 100644 --- a/crates/tui/src/extension_host/tests.rs +++ b/crates/tui/src/extension_host/tests.rs @@ -1131,7 +1131,10 @@ async fn typed_author_example_is_reviewed_before_its_tool_can_execute() { "trust alone does not enable code" ); plugins.enable("hello-extension").unwrap(); - let manager = supervised_manager(&fixture, node); + // This tests plugin review and typed loading, not hang detection. The + // 600 ms watchdog used by supervision fault tests can kill a healthy + // typed-plugin load on a busy runner before registration completes. + let manager = fixture.manager(node); let engine = manager.attach(Arc::new(plugins)); engine.sync().await.unwrap(); let tool = host_tool(&engine, fixture.workspace(), "hello_greet"); From 5947bf1a4848392c02254025bcb429f1a2819ac1 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 21:18:25 -0700 Subject: [PATCH 5/5] test(ci): bound concurrent extension host fixtures Windows full-suite runs on PRs6776 and6780 reported normal extension hosts missing the production five-second handshake deadline. Serialize the host fixture family across nextest processes without changing production deadlines, hang-detection tests, or retries. Nextest profile ci resolves all39 tests (35 extension-host module and4 engine callers) to max-threads1; governed focused run39/39 passed in28.901s on macOS. Exact config SHA256456e681709fe5838e404fbf537f6a934d700d0577ec15bf6a18eeae17aecde53. Full npm740/0 and check:web passed. Windows resolution still requires new hosted runs. Signed-off-by: Hunter B PR #6786 recovery validation: npm test 767 passed, 0 failed (68 wrapper, 16 SDK, 54 extension, 629 web); check:web passed. Focused locale-layout, locale registry, and middleware tests: 56 passed, 0 failed. Removing the two locale guards produced 12 intended failures and 19 passes; exact source restored. Built local server: 17 HTTP probes passed, including dotted parent/child paths and localized missing routes as 404/noindex/no canonical, plus known pages and static assets as 200. Browser surfaces unavailable, so no visual QA claim. Shared Rust fixture validation is the separately recorded parent receipt; no Rust rebuild for this web-only change. --- .config/nextest.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index 4162cf17f2..6c4c8fe146 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -44,6 +44,11 @@ exec-persistent-service = { max-threads = 1 } # (a durable heartbeat and a `ps` sample on every scheduler tick) is now # bounded to once a second per worker. fleet-manager-lifecycle = { max-threads = 1 } +# Extension fixtures launch real Node hosts with a five-second production +# handshake deadline. Windows full-suite runs on #6776/#6780 reported ordinary +# hosts missing that deadline. Bound contention across nextest processes; +# keep production deadlines and the explicit hang-detection tests unchanged. +extension-host = { max-threads = 1 } # First matching test-group override wins. Keep these more-specific # integration filters before binary(integration) so they are not stolen @@ -70,6 +75,10 @@ test-group = 'telemetry-contract' filter = 'package(codewhale-tui) & kind(lib) & test(/^fleet::manager::tests::/)' test-group = 'fleet-manager-lifecycle' +[[profile.default.overrides]] +filter = 'package(codewhale-tui) & kind(lib) & (test(/^extension_host::/) | test(/^core::engine::.*extension/))' +test-group = 'extension-host' + [[profile.default.overrides]] filter = 'binary(integration) & test(/^exec_persistent_service::/)' test-group = 'exec-persistent-service'