From 9973b997cb71e28c2b8aef7f2bd06729a3bcbc21 Mon Sep 17 00:00:00 2001 From: moonyue-w <300878504+moonyue-w@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:52:41 +0800 Subject: [PATCH] fix(test): align live lifecycles with API constraints --- src/forward/schedule.ts | 2 +- .../fixtures/forward/api-operation-cases.json | 3 ++- tests/live/forward-scenarios.mjs | 8 ++++++-- tests/live/managed-scenarios.mjs | 2 +- tests/live/managed-support.mjs | 4 ++-- tests/scenario-harness.mjs | 18 ++++++++++++++++-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/forward/schedule.ts b/src/forward/schedule.ts index c1db1cc..95c9ada 100644 --- a/src/forward/schedule.ts +++ b/src/forward/schedule.ts @@ -48,7 +48,7 @@ export class Schedules extends APIResource { return this._client.request({ method: "POST", path, - body: payload, + body: { ...payload, scope: "by_schedule_ids" }, ...options, headers: requestHeaders({ "Idempotency-Key": idempotency_key }, options?.headers), }); diff --git a/tests/fixtures/forward/api-operation-cases.json b/tests/fixtures/forward/api-operation-cases.json index 8809047..2d7497f 100644 --- a/tests/fixtures/forward/api-operation-cases.json +++ b/tests/fixtures/forward/api-operation-cases.json @@ -141,7 +141,8 @@ "value": { "schedule_ids": [ "sdk-contract" - ] + ], + "scope": "by_schedule_ids" } } }, diff --git a/tests/live/forward-scenarios.mjs b/tests/live/forward-scenarios.mjs index abdf06a..18fb1b3 100644 --- a/tests/live/forward-scenarios.mjs +++ b/tests/live/forward-scenarios.mjs @@ -100,9 +100,10 @@ scenario('environment_lifecycle', async (s) => { scenario('session_resource_thread_lifecycle', async (s) => { const identity = await s.identity(); const template = await s.template((await s.environment()).id); + // Create the file first so reverse-order cleanup archives the session before deleting its mounted file. + const file = await s.file('sdk-resource.txt', 'session_resource', 'SDK resource'); const session = await s.session(identity.id, template.id); assert.equal((await s.client.sessions.update(session.id, { title: 'SDK updated session' }, s.options)).title, 'SDK updated session'); - const file = await s.file('sdk-resource.txt', 'session_resource', 'SDK resource'); const resource = await s.client.sessions.resources.add(session.id, { type: 'file', file_id: file.id, mount_path: '/data/workspace/sdk-resource.txt' }, s.options); assert.equal(resource.file_id, file.id); assert.equal(resource.mount_path, '/data/workspace/sdk-resource.txt'); @@ -194,7 +195,10 @@ scenario('schedule_lifecycle', async (s) => { const template = await s.template(environment.id); const schedule = await s.client.schedules.create({ identity_id: identity.id, template_id: template.id, environment_id: environment.id, name: liveName('schedule'), initial_events: [{ type: 'user.message', content: 'Reply with SDK-LIVE.' }], trigger_policy: { type: 'manual' } }, s.options); - s.cleanup('schedule', (options) => s.client.schedules.archive(schedule.id, {}, options)); + s.cleanup('schedule', async (options) => { + const current = await s.client.schedules.retrieve(schedule.id, options); + if (!current.archived_at) await s.client.schedules.archive(schedule.id, {}, options); + }); assert.equal((await s.client.schedules.retrieve(schedule.id, s.options)).id, schedule.id); assert.equal((await s.client.schedules.update(schedule.id, { description: 'SDK updated schedule' }, s.options)).description, 'SDK updated schedule'); assert.equal((await s.client.schedules.pause(schedule.id, {}, s.options)).status, 'paused'); diff --git a/tests/live/managed-scenarios.mjs b/tests/live/managed-scenarios.mjs index 787a41e..54073dd 100644 --- a/tests/live/managed-scenarios.mjs +++ b/tests/live/managed-scenarios.mjs @@ -54,7 +54,7 @@ export const managedScenarios = [ assert((await download.arrayBuffer()).byteLength > 0, 'Empty skill archive'); }), scenario('environment_lifecycle', 'write', async (s) => { - const environment = await s.createEnvironment(); + const environment = await s.createEnvironment({ type: 'self_hosted' }); const got = await s.client.environments.retrieve(environment.id, {}, s.options()); assert.equal(got.id, environment.id); await s.client.environments.update(environment.id, { description: 'updated through Managed TypeScript SDK' }, s.options()); diff --git a/tests/live/managed-support.mjs b/tests/live/managed-support.mjs index 47321ad..ee64a64 100644 --- a/tests/live/managed-support.mjs +++ b/tests/live/managed-support.mjs @@ -44,8 +44,8 @@ export class ManagedScenarioSuite { if (!model) throw new ScenarioSkip('The Managed account has no enabled model'); return model.id; } - async createEnvironment() { - const environment = await this.client.environments.create({ name: unique('env'), config: { type: 'cloud' }, metadata: { suite: 'sdk-live' } }, this.options()); + async createEnvironment(config = { type: 'cloud' }) { + const environment = await this.client.environments.create({ name: unique('env'), config, metadata: { suite: 'sdk-live' } }, this.options()); this.cleanup(`Environment ${environment.id}`, async () => { try { await this.client.environments.delete(environment.id, {}, this.options()); } catch (error) { diff --git a/tests/scenario-harness.mjs b/tests/scenario-harness.mjs index 9d993e2..ae6da37 100644 --- a/tests/scenario-harness.mjs +++ b/tests/scenario-harness.mjs @@ -165,6 +165,9 @@ export class MockPlatform { } if (action === 'effective') return response(this.records.get(parent + '/config') ?? {}); if (parts[0] === 'qr_sessions') return response(this.get(path)); + if (parts[0] === 'environments' && action === 'work' && this.get(parent).config?.type !== 'self_hosted') { + return response({ error: { message: 'environment is not self-hosted' } }, 400); + } if (route.method === 'list' || route.method === 'listTemplates') { let values = this.data(path); for (const field of ['identity_id','template_id','schedule_id','status']) if (url.searchParams.has(field)) values = values.filter(v => String(v[field]) === url.searchParams.get(field)); @@ -173,9 +176,20 @@ export class MockPlatform { if (!this.records.has(path)) return response({ error: { message: 'not found' } }, 404); return response(clone(this.get(path))); } - if (req.method === 'DELETE') { const item = this.get(path); this.delete(path); return response({ id: item.id, deleted: true }); } + if (req.method === 'DELETE') { + if (parts[0] === 'files' && parts.length === 2) { + const mounted = [...this.records].some(([key, resource]) => { + const match = key.match(/^\/sessions\/([^/]+)\/resources\//); + return match && resource.file_id === parts[1] && !terminal.has(this.records.get(`/sessions/${match[1]}`)?.status); + }); + if (mounted) return response({ error: { message: 'file is mounted by an active session' } }, 409); + } + const item = this.get(path); this.delete(path); return response({ id: item.id, deleted: true }); + } if (action === 'archive' && parts.length === 2 && parts[0] === 'schedules') { - const ids = [...new Set(body.schedule_ids)]; for (const id of ids) this.get(`/schedules/${id}`).status = 'archived'; + assert.equal(body.scope, 'by_schedule_ids', 'schedule archive scope'); + const ids = [...new Set(body.schedule_ids)]; + for (const id of ids) Object.assign(this.get(`/schedules/${id}`), { status: 'archived', archived_at: new Date(0).toISOString() }); return response({ archived_count: ids.length }); } if (['archive','pause','unpause','disable','enable','cancel'].includes(action)) {