Skip unrelated required checks without losing their names (#2542)

* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
This commit is contained in:
Mohamed Boudra
2026-08-02 00:56:05 +08:00
committed by GitHub
parent 429155b536
commit 5a30affd7f
256 changed files with 2693 additions and 2674 deletions
+1
View File
@@ -51,6 +51,7 @@
"test": "npm run test:unit && npm run test:integration",
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts",
"test:hub-cli-contract": "vitest run --maxWorkers=1 src/server/hub/hub-cli-contract.test.ts",
"test:integration:all": "npm run test:e2e",
"test:integration:real": "vitest run real.e2e.test.ts",
"test:integration:local": "vitest run local.e2e.test.ts",
@@ -29,7 +29,7 @@ const openaiApiKey = process.env.OPENAI_API_KEY ?? null;
const localModelsDir =
process.env.PASEO_LOCAL_MODELS_DIR ?? path.join(homedir(), ".paseo", "models", "local-speech");
const testFileDir = path.dirname(fileURLToPath(import.meta.url));
const appE2eFixturesDir = path.resolve(testFileDir, "../../../app/e2e/fixtures");
const appE2eFixturesDir = path.resolve(testFileDir, "../../../app/e2e/support/fixtures");
function fixturePath(fileName: string): string {
return path.join(appE2eFixturesDir, fileName);
@@ -0,0 +1,28 @@
import { afterEach, expect, test } from "vitest";
import { HubRelationshipHarness } from "./test-utils/relationship-harness.js";
let relationship: HubRelationshipHarness | null = null;
afterEach(async () => {
await relationship?.close();
relationship = null;
});
test("the Hub CLI connects, reports status, and disconnects through the daemon", async () => {
relationship = await HubRelationshipHarness.start();
const connected = await relationship.beginConnect().result;
relationship.connectLatestSocket();
const status = await relationship.status();
const enrollment = relationship.enrollmentAttempts()[0];
const secret = relationship.relationshipFile()?.credential?.secret;
const disconnected = await relationship.disconnect();
expect(connected.state).toBe("connecting");
expect(status.state).toBe("connected");
expect(relationship.loggableValues(status)).not.toContain(secret);
expect(relationship.loggableValues(status)).not.toContain(enrollment.credentialVerifier);
expect(relationship.loggableValues(status)).not.toContain(enrollment.token);
expect(relationship.loggableValues(status)).not.toContain(enrollment.idempotencyKey);
expect(disconnected.state).toBe("not_connected");
}, 30_000);
@@ -24,25 +24,6 @@ describe("Hub relationship", () => {
relationship = null;
});
test("the CLI connects, reports status, and disconnects through the daemon", async () => {
relationship = await HubRelationshipHarness.start();
const connected = await relationship.beginConnect().result;
relationship.connectLatestSocket();
const status = await relationship.status();
const enrollment = relationship.enrollmentAttempts()[0];
const secret = relationship.relationshipFile()?.credential?.secret;
const disconnected = await relationship.disconnect();
expect(connected.state).toBe("connecting");
expect(status.state).toBe("connected");
expect(relationship.loggableValues(status)).not.toContain(secret);
expect(relationship.loggableValues(status)).not.toContain(enrollment.credentialVerifier);
expect(relationship.loggableValues(status)).not.toContain(enrollment.token);
expect(relationship.loggableValues(status)).not.toContain(enrollment.idempotencyKey);
expect(disconnected.state).toBe("not_connected");
}, 30_000);
test("Hub URLs cannot persist embedded credentials", async () => {
relationship = await HubRelationshipHarness.start();
@@ -1,4 +1,4 @@
import { createHubCommand } from "../../../../cli/src/commands/hub/index.js";
import { createHubCommand } from "../../../../../cli/src/commands/hub/index.js";
const argv = [process.argv[0] ?? "node", process.argv[1] ?? "paseo", ...process.argv.slice(3)];
await createHubCommand().parseAsync(argv, { from: "node" });
@@ -1328,7 +1328,7 @@ export class HubRelationshipHarness {
}
private async executeCli(args: string[]): Promise<Record<string, unknown>> {
const entrypoint = path.join(import.meta.dirname, "../../test-utils/hub-cli-entry.ts");
const entrypoint = path.join(import.meta.dirname, "hub-cli-entry.ts");
const { stdout } = await execFileAsync(
process.execPath,
[
@@ -4,14 +4,11 @@ import { describe, expect, test } from "vitest";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
import {
AgentSnapshotPayloadSchema,
AgentTimelineItemPayloadSchema,
FetchAgentTimelineResponseMessageSchema,
ServerInfoStatusPayloadSchema,
SessionInboundMessageSchema,
SessionOutboundMessageSchema,
type SessionOutboundMessage,
WSHelloMessageSchema,
} from "@getpaseo/protocol/messages";
import { Session, type SessionOptions } from "./session.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
@@ -44,41 +41,6 @@ const LegacyFetchAgentTimelineResponseMessageSchema = z.object({
}),
});
const LegacySubAgentToolCallSchema = z.object({
type: z.literal("tool_call"),
callId: z.string(),
name: z.string(),
status: z.enum(["running", "completed", "failed", "canceled"]),
error: z.unknown().nullable(),
detail: z.object({
type: z.literal("sub_agent"),
subAgentType: z.string().optional(),
description: z.string().optional(),
log: z.string(),
// Copied from v0.1.65-beta.3: actions was required even though the UI ignored it.
actions: z.array(
z.object({
index: z.number().int().positive(),
toolName: z.string(),
summary: z.string().optional(),
}),
),
}),
});
const LegacyAgentCapabilityFlagsSchema = z.object({
supportsStreaming: z.boolean(),
supportsSessionPersistence: z.boolean(),
supportsDynamicModes: z.boolean(),
supportsMcpServers: z.boolean(),
supportsReasoningStream: z.boolean(),
supportsToolInvocations: z.boolean(),
});
const LegacyAgentSnapshotPayloadSchema = AgentSnapshotPayloadSchema.extend({
capabilities: LegacyAgentCapabilityFlagsSchema,
});
interface SessionInternals {
handleFetchAgentTimelineRequest: (
message: Extract<
@@ -390,84 +352,6 @@ describe("wire compatibility", () => {
]);
});
test("hello parses with and without current client capabilities", () => {
const legacy = WSHelloMessageSchema.parse({
type: "hello",
clientId: "legacy-client",
clientType: "mobile",
protocolVersion: 1,
});
const capable = WSHelloMessageSchema.parse({
type: "hello",
clientId: "capable-client",
clientType: "mobile",
protocolVersion: 1,
capabilities: {
[CLIENT_CAPS.projectUpdates]: true,
},
});
expect([legacy, capable]).toEqual([
{
type: "hello",
clientId: "legacy-client",
clientType: "mobile",
protocolVersion: 1,
},
{
type: "hello",
clientId: "capable-client",
clientType: "mobile",
protocolVersion: 1,
capabilities: {
project_updates: true,
},
},
]);
});
test("server info strips unknown legacy features while accepting former turn identity", () => {
const parsed = ServerInfoStatusPayloadSchema.parse({
status: "server_info",
serverId: "legacy-server",
features: {
workspaceGithubClone: true,
agentTurnIdentity: true,
},
});
expect(parsed).toEqual({
status: "server_info",
serverId: "legacy-server",
hostname: null,
version: null,
features: { agentTurnIdentity: true },
});
});
test("assistant timeline message ids are optional on the wire", () => {
expect(
AgentTimelineItemPayloadSchema.parse({
type: "assistant_message",
text: "old daemon shape",
}),
).toEqual({
type: "assistant_message",
text: "old daemon shape",
});
expect(
AgentTimelineItemPayloadSchema.parse({
type: "assistant_message",
text: "new daemon shape",
messageId: "msg-1",
}),
).toEqual({
type: "assistant_message",
text: "new daemon shape",
messageId: "msg-1",
});
});
test("downgrades reasoning_merge for clients that do not declare the capability", async () => {
const response = await emitTimelineResponse();
@@ -487,100 +371,6 @@ describe("wire compatibility", () => {
expect(currentParsed.payload.entries[0]?.collapsed).toContain("reasoning_merge");
});
test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => {
const parsed = LegacySubAgentToolCallSchema.parse({
type: "tool_call",
callId: "call-sub-agent-1",
name: "Task",
status: "completed",
error: null,
detail: {
type: "sub_agent",
subAgentType: "Explore",
description: "Inspect repository structure",
childSessionId: "child-session-1",
log: "[Read] README.md",
actions: [],
},
});
expect(parsed.detail.actions).toEqual([]);
});
test("old clients parse agent snapshots with rewind capabilities", () => {
const parsed = LegacyAgentSnapshotPayloadSchema.parse({
id: "agent-1",
provider: "claude",
cwd: "/tmp/project",
model: null,
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-05-23T00:00:00.000Z",
updatedAt: "2026-05-23T00:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
supportsRewindConversation: true,
supportsRewindFiles: true,
supportsRewindBoth: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
labels: {},
});
expect(parsed.capabilities).toEqual({
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
});
});
test("new clients parse agent snapshots without rewind capabilities", () => {
const parsed = AgentSnapshotPayloadSchema.parse({
id: "agent-1",
provider: "claude",
cwd: "/tmp/project",
model: null,
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-05-23T00:00:00.000Z",
updatedAt: "2026-05-23T00:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
labels: {},
});
expect(parsed.capabilities.supportsRewindConversation).toBe(false);
expect(parsed.capabilities.supportsRewindFiles).toBe(false);
expect(parsed.capabilities.supportsRewindBoth).toBe(false);
});
test("legacy worktree request shape normalizes to the same internal input as the new shape", async () => {
const workflow = new InMemoryWorktreeWorkflow();
@@ -1,8 +1,7 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { delimiter, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { isPlatform } from "../../../test-utils/platform.js";
import { buildTerminalEnvironment } from "../../terminal.js";
@@ -15,8 +14,6 @@ import {
} from "../provider-registry.js";
const temporaryDirs: string[] = [];
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../..");
afterEach(() => {
while (temporaryDirs.length > 0) {
const dir = temporaryDirs.pop();
@@ -167,17 +164,14 @@ describe("Claude terminal agent hooks", () => {
},
);
it("keeps provider names out of generic CLI and bootstrap integration points", () => {
const genericFiles = [
join(repositoryRoot, "packages", "cli", "src", "commands", "hooks.ts"),
join(repositoryRoot, "packages", "server", "src", "server", "bootstrap.ts"),
];
it("keeps provider names out of the generic server bootstrap", () => {
const source = readFileSync(
new URL("../../../server/bootstrap.ts", import.meta.url),
"utf8",
).toLowerCase();
for (const filePath of genericFiles) {
const contents = readFileSync(filePath, "utf8").toLowerCase();
for (const providerId of Object.keys(AGENT_HOOK_PROVIDERS)) {
expect(contents).not.toContain(providerId);
}
for (const providerId of Object.keys(AGENT_HOOK_PROVIDERS)) {
expect(source).not.toContain(providerId);
}
});
+1 -1
View File
@@ -26,7 +26,7 @@
"exclude": [
"node_modules",
"dist",
"src/server/test-utils/**",
"src/server/**/test-utils/**",
"src/server/daemon-e2e/**",
"src/server/**/*.e2e.ts",
"src/server/**/*.e2e.tsx",