mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(desktop, executions): fix desktop update script and throw error when thinking enabled on stream false (#6578)
* Fix update check * fix include tool calls error * fix comments
This commit is contained in:
@@ -1021,7 +1021,7 @@
|
||||
"post": {
|
||||
"operationId": "executeWorkflowV2",
|
||||
"summary": "Execute Workflow",
|
||||
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with `error.details.code: \"RUN_ID_CONFLICT\"` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: \"failed\"` and `error.code: \"TIMEOUT\"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with `error.details.code: \"RUN_ID_CONFLICT\"` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"tags": ["Workflows"],
|
||||
"security": [
|
||||
{
|
||||
@@ -1069,7 +1069,7 @@
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -3861,12 +3861,12 @@
|
||||
},
|
||||
"includeThinking": {
|
||||
"default": false,
|
||||
"description": "Include model reasoning events in an agent-event stream. Requires the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.",
|
||||
"description": "Include model reasoning events in an agent-event stream. Requires `stream: true` and the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"includeToolCalls": {
|
||||
"default": false,
|
||||
"description": "Include tool-call events in an agent-event stream. Requires the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.",
|
||||
"description": "Include tool-call events in an agent-event stream. Requires `stream: true` and the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"includeFileBase64": {
|
||||
@@ -3882,7 +3882,7 @@
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"title": "Execute workflow request",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"description": "Input and execution-mode options for a deployed workflow. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.",
|
||||
"examples": [
|
||||
{
|
||||
"input": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { setEnv } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { MANIFEST_ASSET_NAME } from '@/lib/desktop/update-feed'
|
||||
@@ -27,8 +28,11 @@ function manifest(version: string) {
|
||||
return [`version: ${version}`, 'files:', ` - url: Sim-${version}-universal-mac.zip`].join('\n')
|
||||
}
|
||||
|
||||
async function getFeed(hostname: string): Promise<Response> {
|
||||
return GET(new NextRequest(`https://${hostname}/api/desktop/update/latest-mac.yml`), undefined)
|
||||
async function getFeed(hostname: string, headers?: HeadersInit): Promise<Response> {
|
||||
return GET(
|
||||
new NextRequest(`https://${hostname}/api/desktop/update/latest-mac.yml`, { headers }),
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
describe('desktop update manifest route', () => {
|
||||
@@ -37,6 +41,7 @@ describe('desktop update manifest route', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockReset()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
setEnv({ APPCONFIG_ENVIRONMENT: undefined })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -44,10 +49,11 @@ describe('desktop update manifest route', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['www.dev.sim.ai', 'v1.2.0-dev.4', '1.2.0-dev.4'],
|
||||
['www.staging.sim.ai', 'v1.2.0-staging.5', '1.2.0-staging.5'],
|
||||
['www.sim.ai', 'v1.1.0', '1.1.0'],
|
||||
])('serves the newest release for %s', async (hostname, tag, version) => {
|
||||
['dev', 'v1.2.0-dev.4', '1.2.0-dev.4'],
|
||||
['staging', 'v1.2.0-staging.5', '1.2.0-staging.5'],
|
||||
['production', 'v1.1.0', '1.1.0'],
|
||||
])('serves the newest release for the %s deployment', async (environment, tag, version) => {
|
||||
setEnv({ APPCONFIG_ENVIRONMENT: environment })
|
||||
fetchMock.mockImplementation(async (input: string | URL | Request) => {
|
||||
const url = String(input)
|
||||
if (url === RELEASES_URL) {
|
||||
@@ -63,7 +69,7 @@ describe('desktop update manifest route', () => {
|
||||
return new Response(null, { status: 404 })
|
||||
})
|
||||
|
||||
const response = await getFeed(hostname)
|
||||
const response = await getFeed('internal.service.local')
|
||||
const body = await response.text()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
@@ -74,6 +80,62 @@ describe('desktop update manifest route', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['dev', 'www.staging.sim.ai:443', 'v1.2.0-dev.4', '1.2.0-dev.4'],
|
||||
['staging', 'www.sim.ai:443', 'v1.2.0-staging.5', '1.2.0-staging.5'],
|
||||
['production', 'www.dev.sim.ai:443', 'v1.1.0', '1.1.0'],
|
||||
])(
|
||||
'ignores request-controlled host headers for the %s deployment',
|
||||
async (environment, spoofedHost, tag, version) => {
|
||||
setEnv({ APPCONFIG_ENVIRONMENT: environment })
|
||||
fetchMock.mockImplementation(async (input: string | URL | Request) => {
|
||||
const url = String(input)
|
||||
if (url === RELEASES_URL) {
|
||||
return Response.json([
|
||||
release('v1.2.0-dev.4'),
|
||||
release('v1.2.0-staging.5'),
|
||||
release('v1.1.0'),
|
||||
])
|
||||
}
|
||||
if (url === `https://downloads.example/${tag}/${MANIFEST_ASSET_NAME}`) {
|
||||
return new Response(manifest(version))
|
||||
}
|
||||
return new Response(null, { status: 404 })
|
||||
})
|
||||
|
||||
const response = await getFeed('internal.service.local', {
|
||||
host: spoofedHost,
|
||||
'x-forwarded-host': `attacker.example, ${spoofedHost}`,
|
||||
})
|
||||
const body = await response.text()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get(FEED_STATUS_HEADER)).toBe('release')
|
||||
expect(body).toContain(`version: ${version}`)
|
||||
}
|
||||
)
|
||||
|
||||
it('defaults self-hosted deployments to the stable channel', async () => {
|
||||
fetchMock.mockImplementation(async (input: string | URL | Request) => {
|
||||
const url = String(input)
|
||||
if (url === RELEASES_URL) {
|
||||
return Response.json([release('v1.2.0-dev.4'), release('v1.1.0')])
|
||||
}
|
||||
if (url === `https://downloads.example/v1.1.0/${MANIFEST_ASSET_NAME}`) {
|
||||
return new Response(manifest('1.1.0'))
|
||||
}
|
||||
return new Response(null, { status: 404 })
|
||||
})
|
||||
|
||||
const response = await getFeed('internal.service.local', {
|
||||
host: 'www.dev.sim.ai:443',
|
||||
'x-forwarded-host': 'www.dev.sim.ai:443',
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.text()).toContain('version: 1.1.0')
|
||||
})
|
||||
|
||||
it('reports an authoritative no-release result for production with only prereleases', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
Response.json([release('v1.2.0-dev.4'), release('v1.2.0-staging.5')])
|
||||
@@ -88,6 +150,7 @@ describe('desktop update manifest route', () => {
|
||||
})
|
||||
|
||||
it('rejects a manifest whose version does not match its selected release', async () => {
|
||||
setEnv({ APPCONFIG_ENVIRONMENT: 'dev' })
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(Response.json([release('v1.2.0-dev.4')]))
|
||||
.mockResolvedValueOnce(new Response(manifest('1.2.0-staging.5')))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import {
|
||||
channelForHostname,
|
||||
channelForDeploymentEnvironment,
|
||||
DESKTOP_RELEASE_REPO,
|
||||
type DesktopReleaseCandidate,
|
||||
MANIFEST_ASSET_NAME,
|
||||
@@ -29,12 +30,14 @@ const RELEASES_API_URL = `https://api.github.com/repos/${DESKTOP_RELEASE_REPO}/r
|
||||
* design: the updater's HTTP client carries no session, and the response
|
||||
* only describes public GitHub release artifacts.
|
||||
*/
|
||||
export const GET = withRouteHandler(async (request: NextRequest): Promise<Response> => {
|
||||
// The same deployment configuration can be promoted across environments, so
|
||||
// its baked NEXT_PUBLIC_APP_URL is not authoritative for this public feed.
|
||||
// The hostname the installed shell actually requested is the channel:
|
||||
// dev -> dev, staging -> staging, and prod/self-hosted -> stable.
|
||||
const channel = channelForHostname(request.nextUrl.hostname)
|
||||
export const GET = withRouteHandler(async (_request: NextRequest): Promise<Response> => {
|
||||
/**
|
||||
* Hosted deployments inject APPCONFIG_ENVIRONMENT independently at runtime,
|
||||
* so it stays correct when the same image is promoted across environments.
|
||||
* Request host headers are intentionally excluded: this public route must not
|
||||
* let a caller choose which app-identity release the feed serves.
|
||||
*/
|
||||
const channel = channelForDeploymentEnvironment(env.APPCONFIG_ENVIRONMENT)
|
||||
|
||||
// A token raises the GitHub API quota from 60/h per NAT IP to 5000/h.
|
||||
// Optional: the repo is public, so the feed works without one.
|
||||
|
||||
@@ -408,6 +408,27 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
|
||||
expect(mockPreprocessExecution).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['includeThinking', 'includeToolCalls'])(
|
||||
'rejects %s unless stream is true before checking the protocol header',
|
||||
async (option) => {
|
||||
const withProtocol = await callExecute(
|
||||
{ [option]: true },
|
||||
{ 'X-Sim-Stream-Protocol': 'agent-events-v1' }
|
||||
)
|
||||
const withoutProtocol = await callExecute({ [option]: true })
|
||||
|
||||
expect(withProtocol.status).toBe(400)
|
||||
expect((await withProtocol.json()).error.message).toBe(
|
||||
'includeThinking and includeToolCalls require stream: true'
|
||||
)
|
||||
expect(withoutProtocol.status).toBe(400)
|
||||
expect((await withoutProtocol.json()).error.message).toBe(
|
||||
'includeThinking and includeToolCalls require stream: true'
|
||||
)
|
||||
expect(mockPreprocessExecution).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('conceals a workspace-key/workflow mismatch as not found', async () => {
|
||||
mockAuthenticateV2ApiKey.mockResolvedValue({
|
||||
principal: {
|
||||
|
||||
@@ -216,13 +216,14 @@ export const POST = withRouteHandler(
|
||||
'Async execution does not support streaming or output-shaping options'
|
||||
)
|
||||
}
|
||||
if (
|
||||
hasAgentStreamPolicy({
|
||||
includeThinking: body.includeThinking,
|
||||
includeToolCalls: body.includeToolCalls,
|
||||
}) &&
|
||||
!clientAcceptsAgentStreamProtocol(req.headers)
|
||||
) {
|
||||
const hasAgentStreamOptions = hasAgentStreamPolicy({
|
||||
includeThinking: body.includeThinking,
|
||||
includeToolCalls: body.includeToolCalls,
|
||||
})
|
||||
if (hasAgentStreamOptions && !body.stream) {
|
||||
return v2Error('BAD_REQUEST', 'includeThinking and includeToolCalls require stream: true')
|
||||
}
|
||||
if (hasAgentStreamOptions && !clientAcceptsAgentStreamProtocol(req.headers)) {
|
||||
return v2Error(
|
||||
'BAD_REQUEST',
|
||||
`includeThinking and includeToolCalls require the ${AGENT_STREAM_PROTOCOL_HEADER_LABEL}: ${AGENT_STREAM_PROTOCOL_V1} request header, which declares that the client understands agent-event frames.`
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
WORKSPACE_API_KEY_DENIED_AS_NOT_FOUND,
|
||||
} from '@/lib/api/contracts/v2/openapi/shared'
|
||||
import {
|
||||
EXECUTE_OPTION_CONSTRAINTS,
|
||||
v2CancelWorkflowRunContract,
|
||||
v2CreateWorkflowContract,
|
||||
v2CreateWorkflowFolderContract,
|
||||
@@ -515,8 +516,7 @@ const routes = [
|
||||
workflowOperation({
|
||||
operationId: 'executeWorkflowV2',
|
||||
summary: 'Execute Workflow',
|
||||
description:
|
||||
'Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with `status: "failed"` and `error.code: "TIMEOUT"` rather than an HTTP error, so branch on `status`. The optional `X-Run-Id` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with `error.details.code: "RUN_ID_CONFLICT"` and never replays the earlier run. Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.',
|
||||
description: `Execute a deployed workflow synchronously, asynchronously, or as Server-Sent Events. Public workflows permit anonymous synchronous and streaming execution; asynchronous execution requires an API key. A synchronous run that exceeds its execution timeout returns HTTP 200 with \`status: "failed"\` and \`error.code: "TIMEOUT"\` rather than an HTTP error, so branch on \`status\`. The optional \`X-Run-Id\` header is a one-shot uniqueness claim, not an idempotency key: reusing a value returns 409 with \`error.details.code: "RUN_ID_CONFLICT"\` and never replays the earlier run. ${EXECUTE_OPTION_CONSTRAINTS}`,
|
||||
errors: [
|
||||
'BadRequest',
|
||||
'Unauthorized',
|
||||
|
||||
@@ -748,7 +748,7 @@ export type V2ExecutionError = z.output<typeof v2ExecutionErrorSchema>
|
||||
* description cannot drift from each other.
|
||||
*/
|
||||
export const EXECUTE_OPTION_CONSTRAINTS =
|
||||
'Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.'
|
||||
'Option constraints — each is a 400: (1) `async: true` requires an API key; anonymous public-workflow callers may only execute synchronously or as a stream. (2) `async` and `stream` cannot both be true. (3) `executionTimeoutSeconds` is accepted only when `async: true`. (4) `async: true` rejects every streaming and output-shaping option — `selectedOutputs`, `includeThinking`, `includeToolCalls`, `includeFileBase64`, and `base64MaxBytes`. (5) `includeThinking` and `includeToolCalls` require `stream: true`. (6) `includeThinking` and `includeToolCalls` require the `X-Sim-Stream-Protocol: agent-events-v1` request header, which declares that the client understands agent-event frames.'
|
||||
|
||||
/**
|
||||
* Strict public execute body. Async is body-selected (`async: true`) — v2 has
|
||||
@@ -756,7 +756,7 @@ export const EXECUTE_OPTION_CONSTRAINTS =
|
||||
* (triggerType, draft state, deployment pinning) are NEVER wire fields; they
|
||||
* are typed options on the execution service.
|
||||
*
|
||||
* The five rejected option combinations are enumerated in
|
||||
* The six rejected option combinations are enumerated in
|
||||
* {@link EXECUTE_OPTION_CONSTRAINTS} and enforced by the route after parsing.
|
||||
*/
|
||||
export const v2ExecuteWorkflowBodySchema = z
|
||||
@@ -806,14 +806,14 @@ export const v2ExecuteWorkflowBodySchema = z
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
'Include model reasoning events in an agent-event stream. Requires the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.'
|
||||
'Include model reasoning events in an agent-event stream. Requires `stream: true` and the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.'
|
||||
),
|
||||
includeToolCalls: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
'Include tool-call events in an agent-event stream. Requires the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.'
|
||||
'Include tool-call events in an agent-event stream. Requires `stream: true` and the `X-Sim-Stream-Protocol: agent-events-v1` request header, and is rejected when `async` is true.'
|
||||
),
|
||||
includeFileBase64: z
|
||||
.boolean()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
channelForHostname,
|
||||
channelForDeploymentEnvironment,
|
||||
channelOfVersion,
|
||||
MANIFEST_ASSET_NAME,
|
||||
rewriteManifestUrls,
|
||||
@@ -22,19 +22,17 @@ function release(
|
||||
}
|
||||
}
|
||||
|
||||
describe('channelForHostname', () => {
|
||||
it('maps hosted environments to their channels', () => {
|
||||
expect(channelForHostname('dev.sim.ai')).toBe('dev')
|
||||
expect(channelForHostname('www.dev.sim.ai')).toBe('dev')
|
||||
expect(channelForHostname('staging.sim.ai')).toBe('staging')
|
||||
expect(channelForHostname('www.staging.sim.ai')).toBe('staging')
|
||||
expect(channelForHostname('sim.ai')).toBe('latest')
|
||||
expect(channelForHostname('www.sim.ai')).toBe('latest')
|
||||
describe('channelForDeploymentEnvironment', () => {
|
||||
it('maps hosted deployment environments to their channels', () => {
|
||||
expect(channelForDeploymentEnvironment('dev')).toBe('dev')
|
||||
expect(channelForDeploymentEnvironment('staging')).toBe('staging')
|
||||
expect(channelForDeploymentEnvironment('production')).toBe('latest')
|
||||
})
|
||||
|
||||
it('defaults self-hosted and local deployments to stable', () => {
|
||||
expect(channelForHostname('sim.example.com')).toBe('latest')
|
||||
expect(channelForHostname('localhost')).toBe('latest')
|
||||
it('defaults self-hosted, local, and unknown deployments to stable', () => {
|
||||
expect(channelForDeploymentEnvironment(undefined)).toBe('latest')
|
||||
expect(channelForDeploymentEnvironment('')).toBe('latest')
|
||||
expect(channelForDeploymentEnvironment('unknown')).toBe('latest')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* GitHub feed, so each environment independently controls which shell build
|
||||
* its clients are offered. The environment IS the channel:
|
||||
*
|
||||
* - dev.sim.ai → `dev` (per-push prerelease builds from `dev`)
|
||||
* - staging.sim.ai → `staging` (per-push prerelease builds from `staging`)
|
||||
* - sim.ai + self-hosted/unknown → `latest` (stable vX.Y.Z releases only)
|
||||
* - hosted `dev` deployment → `dev` (per-push prerelease builds from `dev`)
|
||||
* - hosted `staging` deployment → `staging` (per-push prerelease builds from `staging`)
|
||||
* - production + self-hosted → `latest` (stable vX.Y.Z releases only)
|
||||
*
|
||||
* Artifacts stay on GitHub Releases (dumb storage); the feed route picks the
|
||||
* right release for its channel and serves that release's electron-updater
|
||||
@@ -28,15 +28,12 @@ export const DESKTOP_RELEASE_REPO = 'simstudioai/sim'
|
||||
|
||||
export type DesktopUpdateChannel = 'dev' | 'staging' | 'latest'
|
||||
|
||||
/** Maps a deployment hostname to its desktop update channel. */
|
||||
export function channelForHostname(hostname: string): DesktopUpdateChannel {
|
||||
const host = hostname.toLowerCase()
|
||||
if (host === 'dev.sim.ai' || host.endsWith('.dev.sim.ai')) {
|
||||
return 'dev'
|
||||
}
|
||||
if (host === 'staging.sim.ai' || host.endsWith('.staging.sim.ai')) {
|
||||
return 'staging'
|
||||
}
|
||||
/** Maps Sim's server-controlled deployment environment to its update channel. */
|
||||
export function channelForDeploymentEnvironment(
|
||||
environment: string | undefined
|
||||
): DesktopUpdateChannel {
|
||||
if (environment === 'dev') return 'dev'
|
||||
if (environment === 'staging') return 'staging'
|
||||
return 'latest'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user