Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 || tcpPort > 65_535) {
throw new TypeError("waitForResources requires a positive integer tcpPort");
}

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