diff --git a/.github/scripts/tests/ci-plan.test.mjs b/.github/scripts/tests/ci-plan.test.mjs index c2d561467d8..3285cc3b072 100644 --- a/.github/scripts/tests/ci-plan.test.mjs +++ b/.github/scripts/tests/ci-plan.test.mjs @@ -194,8 +194,8 @@ test("required aggregates reject omitted, failed and cancelled selected work", ( const resultPlan = plan(["ui/src/components/Sidebar.tsx"]); const results = Object.fromEntries(names.map((name) => [name, { result: name === "gate" || name === "policy" || resultPlan.jobs[name]?.run ? "success" : "skipped" }])); - const run = () => spawnSync(process.execPath, ["--input-type=module", "-e", code], { encoding: "utf8", - env: { ...process.env, FULL_CI: "true", CI_PLAN: JSON.stringify(resultPlan), CI_RESULTS: JSON.stringify(results) } }); + const run = (env = {}) => spawnSync(process.execPath, ["--input-type=module", "-e", code], { encoding: "utf8", + env: { ...process.env, FULL_CI: "true", CI_PLAN: JSON.stringify(resultPlan), CI_RESULTS: JSON.stringify(results), ...env } }); assert.equal(run().status, 0); for (const name of names) { // Optional analysis can fail or be skipped; the unchanged task plan @@ -208,6 +208,14 @@ test("required aggregates reject omitted, failed and cancelled selected work", ( } results[name].result = previous; } + for (const result of ["failure", "cancelled", "skipped"]) { + results.gate.result = result; + const failedGate = run({ FULL_CI: "", CI_PLAN: "" }); + assert.notEqual(failedGate.status, 0, `${id}: an absent routing output must not pass`); + assert.match(failedGate.stderr, /CI routing must succeed/); + } + assert.match(block, /^ {4}runs-on: \$\{\{ needs\.gate\.outputs\.runner \|\| 'ubuntu-latest' \}\}$/m, + `${id}: the always-run summary must have a runner when routing produces no output`); } }); diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index 49b12390240..3a435b77553 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -798,7 +798,7 @@ jobs: name: verify if: ${{ always() }} needs: [gate, test_selection, policy, typecheck_release_registry, general_tests, verify_paperclip_runner, build, docker_context_integrity, sandbox_provider_tests, verify_serialized_server, canary_dry_run, verify_runner_e2e_support] - runs-on: ${{ needs.gate.outputs.runner }} + runs-on: ${{ needs.gate.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 5 steps: - name: Validate every planned suite result @@ -1781,7 +1781,7 @@ jobs: name: e2e if: ${{ always() }} needs: [gate, policy, e2e_shards] - runs-on: ${{ needs.gate.outputs.runner }} + runs-on: ${{ needs.gate.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 5 steps: - name: Validate every planned suite result diff --git a/server/src/services/remote-workspace-nested-verification.test.ts b/server/src/services/remote-workspace-nested-verification.test.ts index 7e3494a7730..809b42f3f3e 100644 --- a/server/src/services/remote-workspace-nested-verification.test.ts +++ b/server/src/services/remote-workspace-nested-verification.test.ts @@ -223,6 +223,7 @@ describe.skipIf(!support.supported)("bounded nested workspace verification", () if(input.pauseParentPut)await new Promise(()=>{}); return local.putFile(value); }}; + process.send?.({ready:true}); try{const result=await migrateRemoteWorkspace({db,storage,companyId:input.companyId,workspaceId:input.workspaceId, apply:true,signal:AbortSignal.timeout(30000)});process.send?.({result});} catch(error){process.send?.({failed:true,message:error.message});process.exitCode=1;} @@ -230,28 +231,34 @@ describe.skipIf(!support.supported)("bounded nested workspace verification", () `; const launch = (pauseKey: string | null, pauseParentPut = false, masterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY) => { const reads: string[] = [], results: unknown[] = [], failures: string[] = []; + let ready = false; const child = spawn(process.execPath, ["--import", path.join(repoRoot, "cli/node_modules/tsx/dist/loader.mjs"), "--input-type=module", "--eval", body], { cwd: path.join(repoRoot, "server"), detached: true, env: { ...process.env, PAPERCLIP_SECRETS_MASTER_KEY: masterKey, TMPDIR: temporary, DATABASE_URL: database.connectionString, IMPORT_FIXTURE: JSON.stringify({ companyId: f.companyId, workspaceId: f.parent.id, objects: path.join(home, "objects"), endpoint, keys, pauseKey, pauseParentPut }) }, stdio: ["ignore", "pipe", "pipe", "ipc"] }); let stderr = ""; child.stderr!.on("data", bytes => { stderr += bytes; }); - child.stdout!.resume(); child.on("message", (message: { read?: string; result?: unknown; failed?: boolean; message?: string }) => { + child.stdout!.resume(); child.on("message", (message: { read?: string; result?: unknown; failed?: boolean; message?: string; ready?: boolean }) => { + if (message.ready) ready = true; if (message.read) reads.push(message.read); if (message.result) results.push(message.result); if (message.failed && message.message) failures.push(message.message); }); const closed = new Promise<{ code: number | null; signal: string | null }>((resolve, reject) => { child.once("error", reject); child.once("close", (code, signal) => resolve({ code, signal })); }); - return { child, reads, results, failures, closed, error: () => stderr }; + return { child, reads, results, failures, closed, ready: () => ready, error: () => stderr }; }; const first = mode === "partial" ? launch(keys[1]) : launch(null, true); try { + const prematureExit = first.closed.then(result => { throw new Error(`import_ended_before_checkpoint:${result.code}:${first.error()}`); }); + await Promise.race([vi.waitFor(() => { + expect(first.ready(), `import_startup_not_ready:${first.error()}`).toBe(true); + }, { timeout: 30_000, interval: 25 }), prematureExit]); await Promise.race([vi.waitFor(async () => { const [snapshot] = await db.select().from(executionWorkspaceSnapshots).where(eq(executionWorkspaceSnapshots.id, f.snapshots[0].id)); const completed = (await db.select().from(executionWorkspaceSnapshots)).filter(row => row.metadata?.nestedImportVerification); expect(completed).toHaveLength(mode === "partial" ? 1 : keys.length); expect(snapshot).toBeDefined(); - }, { timeout: 15_000, interval: 25 }), first.closed.then(result => { throw new Error(`import_ended_before_checkpoint:${result.code}:${first.error()}`); })]); + }, { timeout: 15_000, interval: 25 }), prematureExit]); process.kill(-first.child.pid!, "SIGKILL"); expect(await first.closed).toMatchObject({ signal: "SIGKILL" }); } finally {