mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
* feat(desktop): isolate pnpm dev:desktop per worktree (MUL-3724) Two worktrees could not run pnpm dev:desktop at once: both grabbed the renderer port 5173 and the single-instance lock keyed by the app name "Multica Canary". The env hooks to override each already existed (DESKTOP_RENDERER_PORT in electron.vite.config.ts, DESKTOP_APP_SUFFIX in src/main/index.ts) but nothing derived per-worktree values. A new dev launcher (scripts/dev.mjs) derives both from the worktree path for linked worktrees only — reusing the same cksum%1000 offset as scripts/init-worktree-env.sh, so renderer port is 5173+offset and the app becomes "Multica Canary <folder>" with its own userData/lock. The primary checkout is untouched; explicit env vars still win. Backend targeting is unchanged (apps/desktop/.env*). Also: brand-dev-electron honors the suffix, turbo globalEnv passes it through, and CONTRIBUTING documents the flow. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): make worktree dev port/suffix collision-safe (MUL-3724) Addresses code review on #4598: - Renderer port base 5173 → 5174 so a worktree whose offset is 0 (e.g. cksum("/tmp/multica-3494") % 1000 === 0) no longer collides with the primary checkout's default 5173. - DESKTOP_APP_SUFFIX is now "<folder>-<offset>" instead of just the folder name, so worktrees that share a basename at different paths (or names that slug to the same fallback) get distinct single-instance locks. Without it the second Electron was still blocked by the shared lock. - Tests: offset-0 port guard, and same-basename-different-path disambiguation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
appSuffixForPath,
|
|
applyWorktreeDevEnv,
|
|
cksum,
|
|
offsetForPath,
|
|
rendererPortForPath,
|
|
} from "./worktree-dev-env.mjs";
|
|
|
|
const cleanups = [];
|
|
afterEach(() => {
|
|
while (cleanups.length) cleanups.pop()();
|
|
});
|
|
|
|
function tmpRoot(kind /* "file" | "dir" | "none" */) {
|
|
const root = mkdtempSync(join(tmpdir(), "wt-"));
|
|
cleanups.push(() => rmSync(root, { recursive: true, force: true }));
|
|
if (kind === "file") writeFileSync(join(root, ".git"), "gitdir: /elsewhere\n");
|
|
else if (kind === "dir") mkdirSync(join(root, ".git"));
|
|
return root;
|
|
}
|
|
|
|
describe("worktree-dev-env", () => {
|
|
it("cksum is byte-compatible with coreutils cksum(1)", () => {
|
|
// `printf '%s' "/tmp/foo" | cksum` → 427878967 8
|
|
expect(cksum(Buffer.from("/tmp/foo"))).toBe(427878967);
|
|
// `printf '' | cksum` → 4294967295 0
|
|
expect(cksum(Buffer.from(""))).toBe(4294967295);
|
|
});
|
|
|
|
it("derives the offset from the path, mod 1000", () => {
|
|
expect(offsetForPath("/tmp/foo")).toBe(427878967 % 1000);
|
|
});
|
|
|
|
it("renderer port is 5174 + offset (5173 reserved for the primary checkout)", () => {
|
|
expect(rendererPortForPath("/tmp/foo")).toBe(5174 + (427878967 % 1000));
|
|
});
|
|
|
|
it("never reuses 5173 even when the offset is 0", () => {
|
|
// POSIX cksum("/tmp/multica-3494") === 1189739000, % 1000 === 0
|
|
expect(offsetForPath("/tmp/multica-3494")).toBe(0);
|
|
expect(rendererPortForPath("/tmp/multica-3494")).toBe(5174);
|
|
expect(rendererPortForPath("/tmp/multica-3494")).not.toBe(5173);
|
|
});
|
|
|
|
it("suffix is '<folder>-<offset>' so it stays recognizable and unique", () => {
|
|
expect(appSuffixForPath("/work/MUL-3724_Desktop")).toBe(
|
|
`mul-3724-desktop-${offsetForPath("/work/MUL-3724_Desktop")}`,
|
|
);
|
|
expect(appSuffixForPath("/work/feat/some thing")).toBe(
|
|
`some-thing-${offsetForPath("/work/feat/some thing")}`,
|
|
);
|
|
// empty/non-ascii slug falls back to "worktree", still disambiguated by offset
|
|
expect(appSuffixForPath("/work/___")).toBe(`worktree-${offsetForPath("/work/___")}`);
|
|
});
|
|
|
|
it("disambiguates worktrees that share a folder name at different paths", () => {
|
|
// Same basename "multica", different parent dirs → different offsets/suffixes,
|
|
// so each gets its own single-instance lock.
|
|
expect(offsetForPath("/tmp/a/multica")).not.toBe(offsetForPath("/tmp/b/multica"));
|
|
expect(appSuffixForPath("/tmp/a/multica")).not.toBe(
|
|
appSuffixForPath("/tmp/b/multica"),
|
|
);
|
|
});
|
|
|
|
it("auto-isolates a linked worktree (.git is a file)", () => {
|
|
const root = tmpRoot("file");
|
|
const env = {};
|
|
applyWorktreeDevEnv(env, { root });
|
|
expect(env.DESKTOP_RENDERER_PORT).toBe(String(rendererPortForPath(root)));
|
|
expect(env.DESKTOP_APP_SUFFIX).toBe(appSuffixForPath(root));
|
|
});
|
|
|
|
it("leaves the primary checkout untouched (.git is a dir)", () => {
|
|
const root = tmpRoot("dir");
|
|
const env = {};
|
|
applyWorktreeDevEnv(env, { root });
|
|
expect(env.DESKTOP_RENDERER_PORT).toBeUndefined();
|
|
expect(env.DESKTOP_APP_SUFFIX).toBeUndefined();
|
|
});
|
|
|
|
it("respects explicit env overrides", () => {
|
|
const root = tmpRoot("file");
|
|
const env = { DESKTOP_RENDERER_PORT: "9999", DESKTOP_APP_SUFFIX: "manual" };
|
|
applyWorktreeDevEnv(env, { root });
|
|
expect(env.DESKTOP_RENDERER_PORT).toBe("9999");
|
|
expect(env.DESKTOP_APP_SUFFIX).toBe("manual");
|
|
});
|
|
|
|
it("fills only the missing knob when one is set explicitly", () => {
|
|
const root = tmpRoot("file");
|
|
const env = { DESKTOP_RENDERER_PORT: "9999" };
|
|
applyWorktreeDevEnv(env, { root });
|
|
expect(env.DESKTOP_RENDERER_PORT).toBe("9999");
|
|
expect(env.DESKTOP_APP_SUFFIX).toBe(appSuffixForPath(root));
|
|
});
|
|
});
|