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:
@@ -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
-1
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user