Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"@types/node": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"wait-on": "^8.0.2"
"vitest": "catalog:"
},
"productName": "T3 Code (Alpha)"
}
8 changes: 5 additions & 3 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { spawn, spawnSync } from "node:child_process";
import { watch } from "node:fs";
import { join } from "node:path";
import waitOn from "wait-on";

import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
import { waitForResources } from "./wait-for-resources.mjs";

const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5733);
const devServerUrl = `http://localhost:${port}`;
Expand All @@ -20,8 +20,10 @@ const forcedShutdownTimeoutMs = 1_500;
const restartDebounceMs = 120;
const childTreeGracePeriodMs = 1_200;

await waitOn({
resources: [`tcp:${port}`, ...requiredFiles.map((filePath) => `file:${filePath}`)],
await waitForResources({
baseDir: desktopDir,
files: requiredFiles,
tcpPort: port,
});

const childEnv = { ...process.env };
Expand Down
119 changes: 119 additions & 0 deletions apps/desktop/scripts/wait-for-resources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as FileSystem from "node:fs/promises";
import * as Net from "node:net";
import * as Path from "node:path";
import * as Timers from "node:timers/promises";

const defaultTcpHosts = ["127.0.0.1", "localhost", "::1"];

async function fileExists(filePath) {
try {
await FileSystem.access(filePath);
return true;
} catch {
return false;
}
}

function tcpPortIsReady({ host, port, connectTimeoutMs = 500 }) {
return new Promise((resolveReady) => {
const socket = Net.createConnection({ host, port });
let settled = false;

const finish = (ready) => {
if (settled) {
return;
}

settled = true;
socket.removeAllListeners();
socket.destroy();
resolveReady(ready);
};

socket.once("connect", () => {
finish(true);
});
socket.once("timeout", () => {
finish(false);
});
socket.once("error", () => {
finish(false);
});
socket.setTimeout(connectTimeoutMs);
});
}

async function resolvePendingResources({ baseDir, files, tcpPort, tcpHosts, connectTimeoutMs }) {
const pendingFiles = [];

for (const relativeFilePath of files) {
const ready = await fileExists(Path.resolve(baseDir, relativeFilePath));
if (!ready) {
pendingFiles.push(relativeFilePath);
}
}

let tcpReady = false;
for (const host of tcpHosts) {
tcpReady = await tcpPortIsReady({
host,
port: tcpPort,
connectTimeoutMs,
});
if (tcpReady) {
break;
}
}

return {
pendingFiles,
tcpReady,
};
}

export async function waitForResources({
baseDir,
files = [],
intervalMs = 100,
timeoutMs = 120_000,
tcpHost,
tcpPort,
connectTimeoutMs = 500,
}) {
if (!Number.isInteger(tcpPort) || tcpPort <= 0) {
throw new TypeError("waitForResources requires a positive integer tcpPort");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate full TCP port range (1–65535).

Line 83 currently allows values above 65535. That can fail later in createConnection with a low-level socket error instead of a clear argument error.

🔧 Proposed fix
-  if (!Number.isInteger(tcpPort) || tcpPort <= 0) {
+  if (!Number.isInteger(tcpPort) || tcpPort <= 0 || tcpPort > 65_535) {
     throw new TypeError("waitForResources requires a positive integer tcpPort");
   }
What is the valid TCP port range accepted by Node.js net.createConnection()?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/wait-for-resources.mjs` around lines 83 - 85, The
current check in waitForResources only ensures tcpPort is a positive integer but
allows values >65535; update the validation in the waitForResources function to
require Number.isInteger(tcpPort) && tcpPort >= 1 && tcpPort <= 65535 and throw
a clear TypeError (e.g., "waitForResources requires a tcpPort in range 1–65535")
when it fails so invalid ports are rejected before net.createConnection is
called.


const startedAt = Date.now();
const tcpHosts = tcpHost ? [tcpHost] : defaultTcpHosts;

while (true) {
const { pendingFiles, tcpReady } = await resolvePendingResources({
baseDir,
files,
tcpPort,
tcpHosts,
connectTimeoutMs,
});

if (pendingFiles.length === 0 && tcpReady) {
return;
}

if (Date.now() - startedAt >= timeoutMs) {
const pendingResources = [];
if (!tcpReady) {
pendingResources.push(tcpHost ? `tcp:${tcpHost}:${tcpPort}` : `tcp:${tcpPort}`);
}
for (const filePath of pendingFiles) {
pendingResources.push(`file:${filePath}`);
}

throw new Error(
`Timed out waiting for desktop dev resources after ${timeoutMs}ms: ${pendingResources.join(", ")}`,
);
}

await Timers.setTimeout(intervalMs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
type TestProviderAdapterHarness,
} from "./TestProviderAdapter.integration.ts";
import { deriveServerPaths, ServerConfig } from "../src/config.ts";
import { WorkspaceEntriesLive } from "../src/workspace/Layers/WorkspaceEntries.ts";

function runGit(cwd: string, args: ReadonlyArray<string>) {
return execFileSync("git", args, {
Expand Down Expand Up @@ -317,6 +318,12 @@ export const makeOrchestrationIntegrationHarness = (
);
const checkpointReactorLayer = CheckpointReactorLive.pipe(
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(
WorkspaceEntriesLive.pipe(
Layer.provideMerge(gitCoreLayer),
Layer.provide(NodeServices.layer),
),
),
);
const orchestrationReactorLayer = OrchestrationReactorLive.pipe(
Layer.provideMerge(runtimeIngestionLayer),
Expand Down
88 changes: 84 additions & 4 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@
});
}

function buildLargeText(lineCount = 20_000): string {

Check warning on line 135 in apps/server/src/git/Layers/GitCore.test.ts

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint(no-unused-vars)

Function 'buildLargeText' is declared but never used.
return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
.join("\n")
.concat("\n");
}

function splitNullSeparatedPaths(input: string): string[] {
return input
.split("\0")
.map((value) => value.trim())
.filter((value) => value.length > 0);
}

// ── Tests ──

it.layer(TestLayer)("git integration", (it) => {
Expand Down Expand Up @@ -174,6 +187,55 @@
);
});

describe("workspace helpers", () => {
it.effect("filterIgnoredPaths chunks large path lists and preserves kept paths", () =>
Effect.gen(function* () {
const cwd = "/virtual/repo";
const relativePaths = Array.from({ length: 340 }, (_, index) => {
const prefix = index % 3 === 0 ? "ignored" : "kept";
return `${prefix}/segment-${String(index).padStart(4, "0")}/${"x".repeat(900)}.ts`;
});
const expectedPaths = relativePaths.filter(
(relativePath) => !relativePath.startsWith("ignored/"),
);

const seenChunks: string[][] = [];
const core = yield* makeIsolatedGitCore((input) => {
if (input.args.join(" ") !== "check-ignore --no-index -z --stdin") {
return Effect.fail(
new GitCommandError({
operation: input.operation,
command: `git ${input.args.join(" ")}`,
cwd: input.cwd,
detail: "unexpected git command in chunking test",
}),
);
}

const chunkPaths = splitNullSeparatedPaths(input.stdin ?? "");
seenChunks.push(chunkPaths);
const ignoredPaths = chunkPaths.filter((relativePath) =>
relativePath.startsWith("ignored/"),
);

return Effect.succeed({
code: ignoredPaths.length > 0 ? 0 : 1,
stdout: ignoredPaths.length > 0 ? `${ignoredPaths.join("\0")}\0` : "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
});
});

const result = yield* core.filterIgnoredPaths(cwd, relativePaths);

expect(seenChunks.length).toBeGreaterThan(1);
expect(seenChunks.flat()).toEqual(relativePaths);
expect(result).toEqual(expectedPaths);
}),
);
});

// ── listGitBranches ──

describe("listGitBranches", () => {
Expand Down Expand Up @@ -414,6 +476,9 @@

it.effect("refreshes upstream behind count after checkout when remote branch advanced", () =>
Effect.gen(function* () {
const services = yield* Effect.services();
const runPromise = Effect.runPromiseWith(services);

const remote = yield* makeTmpDir();
const source = yield* makeTmpDir();
const clone = yield* makeTmpDir();
Expand Down Expand Up @@ -449,12 +514,15 @@
yield* Effect.promise(() =>
vi.waitFor(
async () => {
const details = await Effect.runPromise(core.statusDetails(source));
const details = await runPromise(core.statusDetails(source));
expect(details.branch).toBe(featureBranch);
expect(details.aheadCount).toBe(0);
expect(details.behindCount).toBe(1);
},
{ timeout: 20_000 },
{
timeout: 10_000,
interval: 100,
},
),
);
}),
Expand Down Expand Up @@ -537,7 +605,13 @@
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "fetch") {
fetchArgs = [...input.args];
return Effect.succeed({ code: 0, stdout: "", stderr: "" });
return Effect.succeed({
code: 0,
stdout: "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
});
}
return realGitCore.execute(input);
});
Expand Down Expand Up @@ -591,7 +665,13 @@
const core = yield* makeIsolatedGitCore((input) => {
if (input.args[0] === "fetch") {
return Effect.promise(() =>
waitForReleasePromise.then(() => ({ code: 0, stdout: "", stderr: "" })),
waitForReleasePromise.then(() => ({
code: 0,
stdout: "",
stderr: "",
stdoutTruncated: false,
stderrTruncated: false,
})),
);
}
return realGitCore.execute(input);
Expand Down
Loading
Loading