diff --git a/packages/cli/src/utils/sandbox.test.ts b/packages/cli/src/utils/sandbox.test.ts new file mode 100644 index 00000000000..060e8267cb8 --- /dev/null +++ b/packages/cli/src/utils/sandbox.test.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { parseSandboxImageName } from './sandboxImageName.js'; + +describe('parseSandboxImageName', () => { + it('uses the image basename and tag for container names', () => { + expect(parseSandboxImageName('ghcr.io/qwenlm/qwen-code:0.18.3')).toBe( + 'qwen-code-0.18.3', + ); + }); + + it('handles registry ports without treating them as tags', () => { + expect( + parseSandboxImageName('localhost:5000/team/qwen-code-sandbox:dev'), + ).toBe('qwen-code-sandbox-dev'); + }); + + it('handles registry ports when the image is untagged', () => { + expect(parseSandboxImageName('localhost:5000/team/qwen-code-sandbox')).toBe( + 'qwen-code-sandbox', + ); + }); + + it('drops digests from generated container names', () => { + expect( + parseSandboxImageName( + 'registry.example.com/team/qwen-code-sandbox@sha256:abcdef', + ), + ).toBe('qwen-code-sandbox'); + }); + + it('keeps tags when dropping digests from generated container names', () => { + expect( + parseSandboxImageName( + 'registry.example.com/team/qwen-code-sandbox:dev@sha256:abcdef', + ), + ).toBe('qwen-code-sandbox-dev'); + }); +}); diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 5cabed9f195..cb8df524e2b 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -23,6 +23,7 @@ import { } from '@qwen-code/qwen-code-core'; import { randomBytes } from 'node:crypto'; import { writeStderrLine } from './stdioHelpers.js'; +import { parseSandboxImageName } from './sandboxImageName.js'; const execAsync = promisify(exec); @@ -102,14 +103,6 @@ async function shouldUseCurrentUserInSandbox(): Promise { return false; } -// docker does not allow container names to contain ':' or '/', so we -// parse those out to shorten the name -function parseImageName(image: string): string { - const [fullName, tag] = image.split(':'); - const name = fullName.split('/').at(-1) ?? 'unknown-image'; - return tag ? `${name}-${tag}` : name; -} - function ports(): string[] { return (process.env['SANDBOX_PORTS'] ?? '') .split(',') @@ -603,7 +596,7 @@ export async function start_sandbox( } // name container after image, plus random suffix to avoid conflicts - const imageName = parseImageName(image); + const imageName = parseSandboxImageName(image); const isIntegrationTest = process.env['QWEN_CODE_INTEGRATION_TEST'] === 'true'; let containerName; diff --git a/packages/cli/src/utils/sandboxImageName.ts b/packages/cli/src/utils/sandboxImageName.ts new file mode 100644 index 00000000000..c71ba8fb36e --- /dev/null +++ b/packages/cli/src/utils/sandboxImageName.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Docker does not allow container names to contain ':' or '/', so parse +// registry paths and image tags into a short container-name prefix. +export function parseSandboxImageName(image: string): string { + const imageWithoutDigest = image.split('@')[0] ?? image; + const lastSlash = imageWithoutDigest.lastIndexOf('/'); + const lastColon = imageWithoutDigest.lastIndexOf(':'); + const hasTag = lastColon > lastSlash; + const fullName = hasTag + ? imageWithoutDigest.slice(0, lastColon) + : imageWithoutDigest; + const tag = hasTag ? imageWithoutDigest.slice(lastColon + 1) : undefined; + const name = fullName.split('/').at(-1) || 'unknown-image'; + + return tag ? `${name}-${tag}` : name; +}