Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
989ce48
perf(ci): rebalance CLI coverage shards
prekshivyas Sep 5, 2026
383dfa3
perf(ci): refine hosted shard balance
prekshivyas Sep 5, 2026
0574153
perf(test): isolate gateway runtime unit dependency
prekshivyas Sep 5, 2026
f229e37
merge: update PR #11098 with main
prekshivyas Sep 5, 2026
195c46b
test(ci): close shard review gaps
prekshivyas Sep 5, 2026
ee6ac63
perf(ci): trim repeated plugin dependency work
prekshivyas Sep 5, 2026
4df2007
test(ci): pin plugin compiler ownership
prekshivyas Sep 5, 2026
f52d79f
test(ci): require locked plugin tool versions
prekshivyas Sep 5, 2026
a92f3f1
test(ci): stabilize shard timing contracts
prekshivyas Sep 5, 2026
563aa14
merge: update PR #11098 with main
prekshivyas Sep 5, 2026
ea2c571
Merge remote-tracking branch 'origin/main' into codex/pr11098-mainten…
prekshivyas Sep 5, 2026
be14520
Merge remote-tracking branch 'origin/main' into codex/pr11098-mainten…
prekshivyas Sep 6, 2026
3725f4a
Merge remote-tracking branch 'origin/main' into codex/pr-11098-build-…
cjagwani Sep 8, 2026
ef52644
perf(ci): split slow tests across coverage shards
cjagwani Sep 8, 2026
6727699
merge: update PR #11098 with main
cjagwani Sep 8, 2026
804ce7b
test(ci): avoid credential-shaped redaction literal
cjagwani Sep 8, 2026
42946d4
perf(ci): rebalance serialized coverage lane
cjagwani Sep 9, 2026
5c401c6
perf(ci): retain twelve coverage shards
cjagwani Sep 9, 2026
acffdc4
test(ci): isolate ARM64 GPU prover fixtures
cjagwani Sep 9, 2026
3a3fbfe
test(ci): remove hidden shard setup
cjagwani Sep 9, 2026
7cfb75d
Merge branch 'main' into codex/perf-cli-shard-critical-path
prekshivyas Sep 9, 2026
7b2a2ad
Merge branch 'main' into codex/perf-cli-shard-critical-path
prekshivyas Sep 9, 2026
9d8921c
Merge branch 'main' into codex/perf-cli-shard-critical-path
prekshivyas Sep 9, 2026
fab7f50
docs(messaging): update split onboarding test command
prekshivyas Sep 9, 2026
851d7d9
test(ci): govern extracted suite size
prekshivyas Sep 9, 2026
933cb4e
test(messaging): remove unused configuration fixtures
prekshivyas Sep 9, 2026
824af6f
test(ci): unify timing hint validation
prekshivyas Sep 9, 2026
6457319
test(ci): expose invalid timing records
prekshivyas Sep 9, 2026
5d18469
test(ci): govern extracted suite growth
prekshivyas Sep 9, 2026
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
4 changes: 2 additions & 2 deletions .github/actions/ci-build-typecheck/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ runs:
shell: bash
run: npm run typecheck:cli

- name: Typecheck plugin
- name: Typecheck plugin tests
shell: bash
run: npm --prefix nemoclaw run typecheck
run: npm --prefix nemoclaw exec -- tsc --noEmit -p nemoclaw/tsconfig.test.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why?


- name: Typecheck JavaScript config
shell: bash
Expand Down
6 changes: 5 additions & 1 deletion .github/actions/ci-cli-coverage-merge/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ runs:
shell: bash
env:
NODE_AUTH_TOKEN: ${{ github.event_name == 'push' && github.token || '' }}
run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh"
run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" none

- name: Download compiled CLI artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand Down Expand Up @@ -99,6 +99,10 @@ runs:
--coverage.exclude="test/**/*.ts"
npx tsx scripts/check-coverage-ratchet.mts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json "CLI coverage"

- name: Report CLI timing drift
shell: bash
run: npx tsx scripts/checks/report-cli-test-timing-drift.mts coverage/cli/vitest-results.json ci/cli-test-timing-hints.json

- name: Upload CLI coverage report
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/ci-cli-coverage-shard/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ runs:
shell: bash
env:
NODE_AUTH_TOKEN: ${{ github.event_name == 'push' && github.token || '' }}
run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh"
run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" production

- name: Validate changed live E2E mock parity
if: ${{ inputs.shard == '1' }}
Expand Down
24 changes: 23 additions & 1 deletion .github/actions/ci-install-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@

set -euo pipefail

if [ "$#" -gt 1 ]; then
echo "Usage: ci-install-dependencies.sh [full|production|none]" >&2
exit 1
fi

plugin_install_mode="${1:-full}"
plugin_install_args=(--prefix nemoclaw ci)
case "$plugin_install_mode" in
full) ;;
production)
plugin_install_args+=(--omit=dev)
;;
none) ;;
*)
echo "Unsupported plugin dependency install mode: $plugin_install_mode" >&2
exit 1
;;
esac

candidate_npmrc="$(find . -path './.git' -prune -o -name .npmrc -print -quit)"
if [ -n "$candidate_npmrc" ]; then
echo "Candidate repository npm configuration is not allowed during trusted dependency installation." >&2
Expand Down Expand Up @@ -57,4 +76,7 @@ if [ "$package_mode" = "registry" ] && [ -n "${NODE_AUTH_TOKEN:-}" ]; then
fi

npm ci --ignore-scripts --prefer-offline --no-audit --no-fund --cache "$npm_cache"
npm --prefix nemoclaw ci --ignore-scripts --prefer-offline --no-audit --no-fund --cache "$npm_cache"
if [ "$plugin_install_mode" != "none" ]; then
npm "${plugin_install_args[@]}" \
--ignore-scripts --prefer-offline --no-audit --no-fund --cache "$npm_cache"
fi
406 changes: 205 additions & 201 deletions ci/cli-test-timing-hints.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@
"test": "provides the package token only to trusted main dependency installation",
"category": "security"
},
{
"file": "test/automation/pull-requests/pr-workflow-contract.test.ts",
"test": "keeps %s plugin test typechecking after the trusted production build",
"category": "security"
},
{
"file": "test/repository/plugin-vitest-project.test.ts",
"test": "keeps standalone plugin %s locked to the root test toolchain",
"category": "compatibility"
},
Comment on lines +179 to +188

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove source-shape tests instead of adding exceptions, please

{
"file": "test/automation/pull-requests/pr-workflow-contract.test.ts",
"test": "passes only the base-packaged SDK archive to pull request dependency jobs",
Expand Down
1 change: 0 additions & 1 deletion ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"test/generation/generate-openclaw-config.test.ts": 1898,
"test/installer-integration/install-preflight.test.ts": 3025,
"test/agents/openclaw/runtime/nemoclaw-start.test.ts": 4377,
"test/onboarding/onboard-messaging.test.ts": 1971,
"test/onboarding/onboard-selection.test.ts": 4133
}
}
76 changes: 76 additions & 0 deletions scripts/checks/cli-test-timing-hints.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

interface TimingHintSource {
runId: number;
artifactId: number;
headSha: string;
recordedAt: string;
}

export interface CliTestTimingHints {
schemaVersion: 2;
defaultDurationMs: number;
sources: readonly TimingHintSource[];
files: Readonly<Record<string, number>>;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function parseCliTestTimingHints(value: unknown): CliTestTimingHints {
if (!isRecord(value) || value.schemaVersion !== 2) {
throw new Error("CLI test timing hints must use schemaVersion 2");
}
if (!Number.isSafeInteger(value.defaultDurationMs) || Number(value.defaultDurationMs) <= 0) {
throw new Error("CLI test timing hints require a positive integer defaultDurationMs");
}
if (!Array.isArray(value.sources) || value.sources.length === 0) {
throw new Error("CLI test timing hints require source metadata");
}
const sources = value.sources.map((source) => {
if (!isRecord(source)) throw new Error("Invalid CLI test timing hint source");
const { runId, artifactId, headSha, recordedAt } = source;
if (
!Number.isSafeInteger(runId) ||
Number(runId) <= 0 ||
!Number.isSafeInteger(artifactId) ||
Number(artifactId) <= 0 ||
typeof headSha !== "string" ||
!/^[0-9a-f]{40}$/u.test(headSha) ||
typeof recordedAt !== "string" ||
Number.isNaN(Date.parse(recordedAt))
) {
throw new Error("Invalid CLI test timing hint source");
}
return { runId: Number(runId), artifactId: Number(artifactId), headSha, recordedAt };
});
if (!isRecord(value.files)) {
throw new Error("CLI test timing hints require a files map");
}

const defaultDurationMs = Number(value.defaultDurationMs);
const files: Record<string, number> = {};
for (const [file, durationMs] of Object.entries(value.files)) {
const segments = file.split("/");
if (
file.length === 0 ||
file.startsWith("/") ||
file.includes("\\") ||
segments.includes("..") ||
!Number.isSafeInteger(durationMs) ||
Number(durationMs) <= defaultDurationMs
) {
throw new Error(`Invalid CLI test timing hint: ${file}`);
}
files[file] = Number(durationMs);
}

return {
schemaVersion: 2,
defaultDurationMs,
sources,
files,
};
}
169 changes: 169 additions & 0 deletions scripts/checks/report-cli-test-timing-drift.mts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What's this adding that you can't get from the vitest reports themselves?

Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env -S npx tsx
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { appendFileSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { parseCliTestTimingHints } from "./cli-test-timing-hints.mts";

type VitestFileResult = {
name?: unknown;
startTime?: unknown;
endTime?: unknown;
};

type VitestReport = {
testResults?: unknown;
};

export type TimingDrift = {
file: string;
hintMs?: number;
kind: "faster" | "slower" | "unprofiled";
observedMs: number;
};

const MINIMUM_DELTA_MS = 10_000;
const HINT_RATIO = 2;
const UNPROFILED_RATIO = 3;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function normalizeReportPath(name: string, repoRoot: string): string | undefined {
const absoluteName = path.isAbsolute(name) ? path.resolve(name) : path.resolve(repoRoot, name);
const relativeName = path.relative(repoRoot, absoluteName).split(path.sep).join("/");
if (relativeName === "" || relativeName === ".." || relativeName.startsWith("../")) {
return undefined;
}
return relativeName;
}

function parseFileResults(value: VitestReport, repoRoot: string): Map<string, number> {
if (!Array.isArray(value.testResults)) {
throw new Error("Invalid Vitest timing report");
}

const durations = new Map<string, number>();
for (const [index, candidate] of value.testResults.entries()) {
if (!isRecord(candidate)) {
throw new Error(`Invalid Vitest timing report entry at index ${index}`);
}
const { name, startTime, endTime } = candidate as VitestFileResult;
if (
typeof name !== "string" ||
typeof startTime !== "number" ||
!Number.isFinite(startTime) ||
typeof endTime !== "number" ||
!Number.isFinite(endTime) ||
endTime < startTime
) {
throw new Error(`Invalid Vitest timing report entry at index ${index}`);
}
const file = normalizeReportPath(name, repoRoot);
if (!file) continue;
durations.set(file, Math.max(durations.get(file) ?? 0, Math.round(endTime - startTime)));
}
return durations;
}

export function findCliTestTimingDrift(
reportValue: VitestReport,
hintsValue: unknown,
repoRoot: string,
): TimingDrift[] {
const hints = parseCliTestTimingHints(hintsValue);
const durations = parseFileResults(reportValue, path.resolve(repoRoot));
const drift: TimingDrift[] = [];

for (const [file, observedMs] of durations) {
const hintMs = hints.files[file];
if (hintMs === undefined) {
if (observedMs >= hints.defaultDurationMs * UNPROFILED_RATIO) {
drift.push({ file, kind: "unprofiled", observedMs });
}
continue;
}

const deltaMs = Math.abs(observedMs - hintMs);
if (deltaMs < MINIMUM_DELTA_MS) continue;
if (observedMs >= hintMs * HINT_RATIO) {
drift.push({ file, hintMs, kind: "slower", observedMs });
} else if (hintMs >= observedMs * HINT_RATIO) {
drift.push({ file, hintMs, kind: "faster", observedMs });
}
}

return drift.sort(
(left, right) => right.observedMs - left.observedMs || left.file.localeCompare(right.file),
);
}

function escapeWorkflowCommand(value: string): string {
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
}

function formatDuration(milliseconds: number): string {
return `${(milliseconds / 1_000).toFixed(1)}s`;
}

function driftMessage(item: TimingDrift): string {
if (item.kind === "unprofiled") {
return `Unprofiled test file took ${formatDuration(item.observedMs)}; refresh CLI timing hints`;
}
return `Test file ran ${item.kind} than its timing hint: observed ${formatDuration(item.observedMs)}, hint ${formatDuration(item.hintMs!)}`;
}

export function formatCliTestTimingDriftSummary(drift: readonly TimingDrift[]): string {
if (drift.length === 0) {
return "### CLI test timing drift\n\nNo material timing-hint drift detected.\n";
}
const rows = drift
.slice(0, 20)
.map(
(item) =>
`| \`${item.file}\` | ${item.kind} | ${formatDuration(item.observedMs)} | ${item.hintMs === undefined ? "—" : formatDuration(item.hintMs)} |`,
)
.join("\n");
return `### CLI test timing drift\n\n${drift.length} file(s) need a timing-hint refresh. This report is advisory.\n\n| File | Drift | Observed | Hint |\n| --- | --- | ---: | ---: |\n${rows}\n`;
}

function loadJson(file: string): unknown {
try {
return JSON.parse(readFileSync(path.resolve(file), "utf8"));
} catch (cause) {
throw new Error(`Failed to load ${file}`, { cause });
}
}

function main(): void {
const [reportFile, hintsFile] = process.argv.slice(2);
if (!reportFile || !hintsFile) {
throw new Error(
"Usage: report-cli-test-timing-drift.mts <vitest-results.json> <cli-test-timing-hints.json>",
);
}

const drift = findCliTestTimingDrift(
loadJson(reportFile) as VitestReport,
loadJson(hintsFile),
process.cwd(),
);
const summary = formatCliTestTimingDriftSummary(drift);
process.stdout.write(summary);
for (const item of drift) {
process.stdout.write(
`::warning file=${escapeWorkflowCommand(item.file)},title=CLI test timing drift::${escapeWorkflowCommand(driftMessage(item))}\n`,
);
}
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
}
}

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? "")) {
main();
}
2 changes: 1 addition & 1 deletion src/lib/messaging/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Use the narrowest test that covers the changed surface:
- Hook behavior: `npx vitest run src/lib/messaging/hooks src/lib/messaging/channels/<channel>/hooks`
- Host/OpenShell application: `npx vitest run src/lib/messaging/applier`
- Build-time render/install behavior: `npx vitest run test/runtime/messaging/messaging-build-applier.test.ts`
- Onboard/channel CLI integration: `npx vitest run test/onboarding/onboard-messaging.test.ts test/channels/channels-add-preset.test.ts src/lib/onboard/messaging-channel-setup.test.ts`
- Onboard/channel CLI integration: `npx vitest run test/onboarding/onboard-messaging-image-configuration.test.ts test/onboarding/onboard-messaging-provider.test.ts test/onboarding/onboard-messaging-recreation.test.ts test/channels/channels-add-preset.test.ts src/lib/onboard/messaging-channel-setup.test.ts`

Add focused negative tests for invalid credentials, unauthorized senders, denied network access, malformed configuration, and cleanup when those behaviors are in scope.

Expand Down
8 changes: 7 additions & 1 deletion src/lib/onboard/gateway-host-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,17 @@ describe("gateway host runtime attachment probe", () => {

it("reads the authoritative gateway port lazily, not at construction (#6576)", () => {
let port = 8080;
const runtime = createGatewayHostRuntime(createDeps({ gatewayPort: () => port }));
const getGatewayStartNetworkEnv = vi.fn((gatewayPort: number) => ({
OPENSHELL_SERVER_PORT: String(gatewayPort),
}));
const runtime = createGatewayHostRuntime(
createDeps({ gatewayPort: () => port, getGatewayStartNetworkEnv }),
);

port = 9443;

expect(runtime.getGatewayStartEnv()).toMatchObject({ OPENSHELL_SERVER_PORT: "9443" });
expect(getGatewayStartNetworkEnv).toHaveBeenCalledWith(9443);
}, 15_000);

it("registers and selects the exact declared endpoint without prior gateway metadata (#6576)", async () => {
Expand Down
Loading
Loading