Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
74331e2a96 test(agent): serialize fake-executable writes to avoid ETXTBSY on CI
TestKimiBackendInvokesACPSubcommand (and its Kimi/Codex siblings) write a
shell script to a per-test TempDir and then fork/exec it. With t.Parallel()
enabled across the package, a concurrent goroutine's fork can inherit the
still-open write fd to another test's new executable; Linux then rejects
the subsequent exec with ETXTBSY (seen as
  fork/exec /tmp/.../kimi: text file busy
on GitHub Actions).

Introduce writeTestExecutable, which holds syscall.ForkLock.RLock across
OpenFile→Write→Close. Fork (which takes ForkLock.Lock) cannot run while we
hold RLock, so no sibling fork inherits our write fd. Ran the three callers
with -count=10 under -p=1 and the full package with no failures.
2026-04-23 01:48:43 +08:00
Jiang Bohan
a9dd86744d fix(landing): scope landing route to always-light palette
The landing page sections use hardcoded light colors (bg-white / #0a0d12),
but shared components rendered inside — notably CloudWaitlistExpand on
/download — use semantic tokens that flip to dark values under next-themes'
`.dark` class, producing a mismatched dark card on an otherwise light page
when the user's OS is in dark mode.

Add a `.landing-light` class on the landing layout wrapper that re-declares
all color tokens to their light values for the subtree, so nested
token-driven components stay in lockstep with the hardcoded palette.
2026-04-23 01:28:46 +08:00
5 changed files with 77 additions and 11 deletions

View File

@@ -67,7 +67,7 @@ export default async function LandingLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} h-full overflow-x-hidden overflow-y-auto bg-white`}>
<div className={`${instrumentSerif.variable} ${notoSerifSC.variable} landing-light h-full overflow-x-hidden overflow-y-auto bg-white`}>
<LocaleProvider initialLocale={initialLocale}>{children}</LocaleProvider>
</div>
</>

View File

@@ -3,3 +3,44 @@
* Shared styles (shiki, entrance-spin, sidebar, sonner, scrollbar) are in
* @multica/ui/styles/base.css
* ============================================================================= */
/* The landing route tree is intentionally always-light (hero/cli/cloud
* sections use hardcoded dark/light palettes). Shared components rendered
* inside (e.g. CloudWaitlistExpand on /download) use semantic tokens that
* otherwise flip to dark values under the `.dark` class set by next-themes,
* producing a palette mismatch against the hardcoded section. Re-declare
* tokens to their light values so nested token-driven components stay in
* lockstep with the surrounding design. */
.landing-light,
.landing-light * {
color-scheme: light;
}
.landing-light {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--brand: oklch(0.55 0.16 255);
--brand-foreground: oklch(0.985 0 0);
--success: oklch(0.55 0.16 145);
--warning: oklch(0.75 0.16 85);
--info: oklch(0.55 0.18 250);
--priority: oklch(0.65 0.18 50);
--scrollbar-thumb: oklch(0 0 0 / 10%);
--scrollbar-thumb-hover: oklch(0 0 0 / 18%);
--scrollbar-track: transparent;
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"
@@ -974,9 +973,7 @@ func TestCodexExecuteSurfacesStderrWhenChildExitsEarly(t *testing.T) {
script := "#!/bin/sh\n" +
"echo \"error: unexpected argument '-m' found\" >&2\n" +
"exit 2\n"
if err := os.WriteFile(fakePath, []byte(script), 0o755); err != nil {
t.Fatalf("write fake codex: %v", err)
}
writeTestExecutable(t, fakePath, []byte(script))
backend, err := New("codex", Config{ExecutablePath: fakePath, Logger: slog.Default()})
if err != nil {

View File

@@ -0,0 +1,32 @@
//go:build unix
package agent
import (
"os"
"syscall"
"testing"
)
// writeTestExecutable writes content to path with exec perms while holding
// syscall.ForkLock.RLock, so no concurrent t.Parallel() sibling can fork
// between our OpenFile and Close. Without this, Linux ETXTBSY fires when
// the sibling's fork child inherits our still-open write fd and the
// subsequent exec of the file sees "text file busy" (seen on CI as
// TestKimiBackendInvokesACPSubcommand: fork/exec ... text file busy).
func writeTestExecutable(tb testing.TB, path string, content []byte) {
tb.Helper()
syscall.ForkLock.RLock()
defer syscall.ForkLock.RUnlock()
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
tb.Fatalf("write test executable %s: open: %v", path, err)
}
if _, err := f.Write(content); err != nil {
_ = f.Close()
tb.Fatalf("write test executable %s: write: %v", path, err)
}
if err := f.Close(); err != nil {
tb.Fatalf("write test executable %s: close: %v", path, err)
}
}

View File

@@ -103,9 +103,7 @@ func TestKimiBackendSetModelFailureFailsTask(t *testing.T) {
t.Parallel()
fakePath := filepath.Join(t.TempDir(), "kimi")
if err := os.WriteFile(fakePath, []byte(fakeKimiACPScript()), 0o755); err != nil {
t.Fatalf("write fake kimi: %v", err)
}
writeTestExecutable(t, fakePath, []byte(fakeKimiACPScript()))
backend, err := New("kimi", Config{ExecutablePath: fakePath, Logger: slog.Default()})
if err != nil {
@@ -164,9 +162,7 @@ func TestKimiBackendInvokesACPSubcommand(t *testing.T) {
tempDir := t.TempDir()
argsFile := filepath.Join(tempDir, "argv.txt")
fakePath := filepath.Join(tempDir, "kimi")
if err := os.WriteFile(fakePath, []byte(fakeKimiACPScript()), 0o755); err != nil {
t.Fatalf("write fake kimi: %v", err)
}
writeTestExecutable(t, fakePath, []byte(fakeKimiACPScript()))
backend, err := New("kimi", Config{
ExecutablePath: fakePath,