Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
cache: true
run-install: true

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron

Expand All @@ -31,6 +34,9 @@ jobs:
- name: Typecheck
run: vpr typecheck

- name: Check resource monitor formatting
run: cargo fmt --manifest-path native/resource-monitor/Cargo.toml -- --check

- name: Build desktop pipeline
run: vp run build:desktop

Expand All @@ -54,12 +60,18 @@ jobs:
cache: true
run-install: true

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron

- name: Test
run: vp run test

- name: Test resource monitor
run: cargo test --locked --manifest-path native/resource-monitor/Cargo.toml

test_browser:
name: Test Browser
runs-on: blacksmith-8vcpu-ubuntu-2404
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,21 +272,29 @@ jobs:
platform: mac
target: dmg
arch: arm64
rust_target: aarch64-apple-darwin
resource_key: darwin-arm64
- label: macOS x64
runner: blacksmith-12vcpu-macos-26
platform: mac
target: dmg
arch: x64
rust_target: x86_64-apple-darwin
resource_key: darwin-x64
- label: Linux x64
runner: blacksmith-32vcpu-ubuntu-2404
platform: linux
target: AppImage
arch: x64
rust_target: x86_64-unknown-linux-gnu
resource_key: linux-x64
- label: Windows x64
runner: blacksmith-32vcpu-windows-2025
platform: win
target: nsis
arch: x64
rust_target: x86_64-pc-windows-msvc
resource_key: win32-x64
# - label: Windows arm64
# runner: windows-11-arm
# platform: win
Expand All @@ -306,6 +314,11 @@ jobs:
cache: true
run-install: true

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}

- name: Download relay client tracing config
uses: actions/download-artifact@v8
with:
Expand Down Expand Up @@ -518,13 +531,33 @@ jobs:
# done
# fi

- name: Collect resource monitor
shell: bash
run: |
set -euo pipefail
binary_name="t3-resource-monitor"
if [[ "${{ matrix.platform }}" == "win" ]]; then
binary_name="${binary_name}.exe"
fi
source_path="native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}"
target_dir="resource-monitor-publish/${{ matrix.resource_key }}"
mkdir -p "$target_dir"
cp "$source_path" "$target_dir/$binary_name"

- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: release-publish/*
if-no-files-found: error

- name: Upload resource monitor
uses: actions/upload-artifact@v7
with:
name: resource-monitor-${{ matrix.resource_key }}
path: resource-monitor-publish/${{ matrix.resource_key }}/*
if-no-files-found: error

publish_cli:
name: Publish CLI to npm
needs: [preflight, relay_public_config, build]
Expand Down Expand Up @@ -579,6 +612,24 @@ jobs:
- name: Build CLI package
run: vp run --filter t3 build

- name: Download resource monitors
uses: actions/download-artifact@v8
with:
pattern: resource-monitor-*
path: ${{ runner.temp }}/resource-monitors

- name: Bundle resource monitors into CLI package
shell: bash
run: |
set -euo pipefail
for artifact_dir in "$RUNNER_TEMP"/resource-monitors/resource-monitor-*; do
resource_key="${artifact_dir##*/resource-monitor-}"
target_dir="apps/server/dist/resource-monitor/${resource_key}"
mkdir -p "$target_dir"
cp "$artifact_dir"/t3-resource-monitor* "$target_dir/"
chmod +x "$target_dir"/t3-resource-monitor 2>/dev/null || true
done

- name: Publish CLI package
run: node apps/server/scripts/cli.ts publish --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --app-version "${{ needs.preflight.outputs.version }}" --verbose

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ squashfs-root/
.gstack/
dist-electron/
.electron-runtime/
native/**/target/
node_modules/
.alchemy/
*.log
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>
}),
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
setDesktopName: () => Effect.void,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopCloudAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function makeHarness(input: { readonly isDevelopment: boolean }): CloudAuthHarne
setAboutPanelOptions: () => Effect.void,
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: (protocol, path, args) =>
Effect.sync(() => {
Expand Down
149 changes: 128 additions & 21 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ const environmentInput = (baseDir: string) =>
runningUnderArm64Translation: false,
}) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;

const makeEnvironmentLayer = (baseDir: string) =>
const makeEnvironmentLayer = (baseDir: string, isDevelopment = true) =>
DesktopEnvironment.layer(environmentInput(baseDir)).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({
T3CODE_HOME: baseDir,
VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
VITE_DEV_SERVER_URL: isDevelopment ? "http://127.0.0.1:5733" : undefined,
}),
),
),
Expand Down Expand Up @@ -112,48 +112,155 @@ describe("DesktopObservability", () => {
),
);

it.effect("persists backend child output as structured JSON records in development", () =>
it.effect("buffers backend child output and persists it only when a failure is reported", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-output-log-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir);
const environmentLayer = makeEnvironmentLayer(baseDir, false);
const logPath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "server-child.log");
}).pipe(Effect.provide(environmentLayer));
const tracePath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "desktop.trace.ndjson");
}).pipe(Effect.provide(environmentLayer));

yield* Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
yield* outputLog.writeSessionBoundary({
phase: "START",
details: "pid=123 port=3773 cwd=/repo",
});
yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("hello server\n"));
}).pipe(
Effect.annotateLogs({ runId: "test-run" }),
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
yield* Effect.scoped(
Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
yield* outputLog.beginSession({
details: "pid=123 port=3773 cwd=/repo",
});
yield* outputLog.writeOutputChunk("stdout", new TextEncoder().encode("hello server\n"));
assert.isFalse(yield* fileSystem.exists(logPath));
yield* outputLog.persistFailure({ details: "code=1" });
yield* outputLog.beginSession({ details: "pid=456" });
yield* outputLog.writeOutputChunk(
"stderr",
new TextEncoder().encode("normal shutdown\n"),
);
yield* outputLog.discardSession;
}).pipe(
Effect.annotateLogs({ runId: "test-run" }),
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

const log = yield* fileSystem.readFileString(logPath);
const lines = log.trimEnd().split("\n");
const boundary = yield* decodeDesktopBackendChildLogRecord(lines[0] ?? "");
const start = yield* decodeDesktopBackendChildLogRecord(lines[0] ?? "");
const output = yield* decodeDesktopBackendChildLogRecord(lines[1] ?? "");
const end = yield* decodeDesktopBackendChildLogRecord(lines[2] ?? "");

assert.equal(boundary.message, "backend child process session start");
assert.equal(boundary.level, "INFO");
assert.equal(boundary.annotations.component, "desktop-backend-child");
assert.equal(boundary.annotations.runId, "test-run");
assert.equal(boundary.annotations.phase, "START");
assert.equal(boundary.annotations.details, "pid=123 port=3773 cwd=/repo");
assert.equal(lines.length, 3);
assert.equal(start.message, "backend child process failure output start");
assert.equal(start.level, "ERROR");
assert.equal(start.annotations.component, "desktop-backend-child");
assert.equal(start.annotations.runId, "test-run");
assert.equal(start.annotations.phase, "START");
assert.equal(start.annotations.details, "pid=123 port=3773 cwd=/repo");

assert.equal(output.message, "backend child process output");
assert.equal(output.level, "INFO");
assert.equal(output.annotations.component, "desktop-backend-child");
assert.equal(output.annotations.runId, "test-run");
assert.equal(output.annotations.stream, "stdout");
assert.equal(output.annotations.text, "hello server\n");

assert.equal(end.message, "backend child process failure output end");
assert.equal(end.level, "ERROR");
assert.equal(end.annotations.phase, "END");
assert.equal(end.annotations.details, "code=1");

const traceRecords = (yield* fileSystem.readFileString(tracePath))
.trim()
.split("\n")
.filter((line) => line.length > 0)
.map((line) => decodeTraceRecordLine(line));
assert.isFalse(
traceRecords.some(
(record) => record.name === "desktop.observability.backendOutput.writeOutputChunk",
),
);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
),
);

it.effect("retains only the last mebibyte of backend child output", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-output-bound-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, false);
const logPath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "server-child.log");
}).pipe(Effect.provide(environmentLayer));
const maxBufferedBytes = 1024 * 1024;
const discardedPrefixBytes = 128;
const output = new Uint8Array(maxBufferedBytes + discardedPrefixBytes);
output.fill("x".charCodeAt(0));
output.fill("y".charCodeAt(0), 0, discardedPrefixBytes);

yield* Effect.scoped(
Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
yield* outputLog.beginSession({ details: "pid=123" });
yield* outputLog.writeOutputChunk("stderr", output);
yield* outputLog.persistFailure({ details: "code=1" });
}).pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

const lines = (yield* fileSystem.readFileString(logPath)).trimEnd().split("\n");
const record = yield* decodeDesktopBackendChildLogRecord(lines[1] ?? "");
const text = record.annotations.text;
assert.equal(typeof text, "string");
if (typeof text !== "string") {
return;
}
assert.equal(new TextEncoder().encode(text).byteLength, maxBufferedBytes);
assert.isFalse(text.includes("y"));
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
),
);

it.effect("bounds the number of retained backend child output chunks", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-output-chunks-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, false);
const logPath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "server-child.log");
}).pipe(Effect.provide(environmentLayer));

yield* Effect.scoped(
Effect.gen(function* () {
const outputLog = yield* DesktopObservability.DesktopBackendOutputLog;
yield* outputLog.beginSession({ details: "pid=123" });
for (let index = 0; index < 300; index += 1) {
yield* outputLog.writeOutputChunk("stderr", Uint8Array.of(index % 128));
}
yield* outputLog.persistFailure({ details: "code=1" });
}).pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
),
);

const lines = (yield* fileSystem.readFileString(logPath)).trimEnd().split("\n");
assert.equal(lines.length, 258);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
Expand Down
Loading
Loading