diff --git a/VERSION b/VERSION index 5a5831a..d169b2f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.7 +0.0.8 diff --git a/config/sources.tsv b/config/sources.tsv index 2d61d68..9546516 100644 --- a/config/sources.tsv +++ b/config/sources.tsv @@ -8,6 +8,7 @@ chatgpt-download distribution https://persistent.oaistatic.com/codex-app-prod/Co chatgpt-distribution distribution https://persistent.oaistatic.com/codex-app-prod/ chatgpt-releases metadata https://persistent.oaistatic.com/codex-app-prod/appcast.xml kiro-distribution distribution https://prod.download.desktop.kiro.dev/ +kiro-releases metadata https://prod.download.desktop.kiro.dev/stable/metadata-dmg-darwin-arm64-stable.json kiro-cli-distribution distribution https://prod.download.cli.kiro.dev/stable/ kiro-cli-releases metadata https://prod.download.cli.kiro.dev/stable/latest/manifest.json claude-releases metadata https://downloads.claude.ai/claude-code-releases/latest diff --git a/docs/sources.md b/docs/sources.md index 24da999..ddce0a9 100644 --- a/docs/sources.md +++ b/docs/sources.md @@ -1,8 +1,8 @@ # Sources and update procedure -Source baseline reviewed 2026-09-28. [`config/sources.tsv`](../config/sources.tsv) is the only executable allowlist for native installers, distributions, metadata endpoints, and the Powerlevel10k repository. Editing a link in this document does not change installer behavior. Any source change must also update the manifest fixture and pass the repository checks. +Source baseline reviewed 2026-10-27. [`config/sources.tsv`](../config/sources.tsv) is the only executable allowlist for native installers, distributions, metadata endpoints, and the Powerlevel10k repository. Editing a link in this document does not change installer behavior. Any source change must also update the manifest fixture and pass the repository checks. -Homebrew formulae and casks use the source declared by their official definitions. The default Doubao flow reads current cask metadata from Homebrew's official JSON API, while optional `--with-sogou` reads the local cask metadata. Both additionally restrict the resolved URL to its official distribution prefix in `sources.tsv` and download the fixed-checksum ZIP without installing it. +Homebrew formulae and casks use the source declared by their official definitions. The default Doubao flow reads current cask metadata from Homebrew's official JSON API, accepts both its original build-only filename and its current build-plus-release filename, and requires every filename component to match the corresponding cask version field. Optional `--with-sogou` reads the local cask metadata. Both additionally restrict the resolved URL to its official distribution prefix in `sources.tsv` and download the fixed-checksum ZIP without installing it. ## Installer and configuration sources @@ -34,7 +34,7 @@ Homebrew formulae and casks use the source declared by their official definition Default mode checks each selected desktop app and downloads an official DMG when it is absent. Docker Desktop requires `--with-docker` after the user reviews the current vendor terms. iTerm2 and VS Code use casks. AI CLIs prefer official scripts. `--managed-desktop` enables app placement and managed Kiro hooks without selecting Docker. -ChatGPT uses the fixed official DMG path and its appcast. Kiro selects the stable arm64 DMG from the official download page. Claude Desktop is accessed only with `--with-claude`, first through the vendor's latest-DMG redirect and then through the official release feed as a fallback. Chrome and selected Docker Desktop use official rolling DMG addresses. A normal rerun reuses selected packages whose digests still match; `--update` refreshes them. +ChatGPT uses the fixed official DMG path and its appcast. Kiro selects the exact current stable arm64 DMG from the vendor's architecture-specific release metadata. Claude Desktop is accessed only with `--with-claude`, first through the vendor's latest-DMG redirect and then through the official release feed as a fallback. Chrome and selected Docker Desktop use official rolling DMG addresses. A normal rerun reuses selected packages whose digests still match; `--update` refreshes them. Claude Code and Codex use their own native installers and metadata. Default Kiro CLI runs the vendor installer and preserves its replacement prompt and onboarding. Managed Kiro resolves a digest from the official stable manifest. Recorded hashes support caching and audit; they do not represent an independent vendor signature. diff --git a/scripts/battery.sh b/scripts/battery.sh index a88ac1a..074c115 100755 --- a/scripts/battery.sh +++ b/scripts/battery.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -BATTERY_TOOL_VERSION=0.0.7 +BATTERY_TOOL_VERSION=0.0.8 BATTERY_SCHEMA_VERSION=1 MAX_RETAINED_EVENTS=256 diff --git a/scripts/official_ai.py b/scripts/official_ai.py index 0259b29..8007f08 100644 --- a/scripts/official_ai.py +++ b/scripts/official_ai.py @@ -86,17 +86,24 @@ def release(name, catalog=None, fetch=fetch_text): _, _, version, build = max(entries) return dict(version=version, build=build, url=catalog['chatgpt-download']) if name == 'kiro': - entries = [] + data = json.loads(fetch(catalog['kiro-releases'])) + if not isinstance(data, dict): + raise ValueError('Unexpected Kiro stable release metadata') + version = data.get('currentRelease') + releases = data.get('releases') + if (not isinstance(version, str) or not re.fullmatch(r'\d+\.\d+\.\d+', version) + or not isinstance(releases, list) or not all(isinstance(item, dict) for item in releases)): + raise ValueError('Unexpected Kiro stable release metadata') + version_key(version) + entries = [item['updateTo'] for item in releases + if item.get('version') == version and isinstance(item.get('updateTo'), dict)] + if len(entries) != 1 or entries[0].get('version') != version: + raise ValueError('Unexpected Kiro stable release metadata') prefix = catalog['kiro-distribution'] - for url in re.findall(r'https://[^\s"<>\\]+', fetch(catalog['kiro'])): - if not url.startswith(prefix): - continue - match = re.fullmatch(r'releases/stable/darwin-arm64/signed/(\d+\.\d+\.\d+)/kiro-ide-\1-stable-darwin-arm64\.dmg', url[len(prefix):]) - if match: - entries.append((version_key(match[1]), match[1], official_url(url, prefix))) - if not entries: - raise ValueError('No stable Apple Silicon Kiro DMG on official download page') - _, version, url = max(entries) + url = official_url(entries[0].get('url', ''), prefix) + expected = f'releases/stable/darwin-arm64/signed/{version}/kiro-ide-{version}-stable-darwin-arm64.dmg' + if url[len(prefix):] != expected: + raise ValueError('Unexpected Kiro stable Apple Silicon DMG') return dict(version=version, url=url) if name == 'kiro-cli': data = json.loads(fetch(catalog['kiro-cli-releases'])) diff --git a/scripts/prepare-doubao.py b/scripts/prepare-doubao.py index 5f88e6b..1365c6b 100755 --- a/scripts/prepare-doubao.py +++ b/scripts/prepare-doubao.py @@ -31,10 +31,15 @@ def metadata(): prefix = sources['doubao-input-download'] match = re.fullmatch( r'https://lf-wave\.doubaocdn\.com/obj/doubao-ime/app/macos/' - r'DoubaoImeInstaller_v([0-9]+)_release\.zip', item['url']) + r'DoubaoImeInstaller_v([0-9]+)_release' + r'(?:_([0-9]+(?:_[0-9]+)*))?\.zip', item['url']) versions = item['version'].split(',') + release = match.group(2) if match else None + release_matches = ((len(versions) == 2 and release is None) + or (len(versions) == 3 and release == versions[2] + and re.fullmatch(r'[0-9]+(?:_[0-9]+)*', versions[2]))) if (item['token'] != 'doubaoime' or not item['url'].startswith(prefix) or not match - or len(versions) != 2 or not re.fullmatch(r'[0-9]+(?:\.[0-9]+)+', versions[0]) + or not release_matches or not re.fullmatch(r'[0-9]+(?:\.[0-9]+)+', versions[0]) or not re.fullmatch(r'[0-9]+', versions[1]) or match.group(1) != versions[1] or not re.fullmatch(r'[a-f0-9]{64}', item['sha256'])): raise ValueError('Doubao source/checksum format changed; review the official cask before updating.') diff --git a/tests/fixtures/config-golden.json b/tests/fixtures/config-golden.json index 3a2dbb2..e84bada 100644 --- a/tests/fixtures/config-golden.json +++ b/tests/fixtures/config-golden.json @@ -24,7 +24,7 @@ "obsidian-vault/_templates/.gitkeep": "\n", "repository-files.txt": ".gitignore\n.github/pull_request_template.md\n.github/workflows/quality.yml\n1bite\nAGENTS.md\nLICENSE\nMakefile\nREADME.md\nVERSION\napps/README.md\napps/qbittorrent/README.md\napps/qbittorrent/compose.yaml\napps/qbittorrent/qbt.py\napps/qbittorrent/qbt.sh\napps/qbittorrent/smoke-test.py\nassets/logo.png\nconfig/casks.tsv\nconfig/claude-settings.json\nconfig/codex-config.toml\nconfig/download.curlrc\nconfig/env.zsh\nconfig/font-casks.tsv\nconfig/formulae.txt\nconfig/git-defaults.json\nconfig/gitignore-global\nconfig/iterm2-profile.json\nconfig/kiro-permissions.json\nconfig/obsidian-vault/.gitignore\nconfig/obsidian-vault/.obsidian/app.json\nconfig/obsidian-vault/.obsidian/appearance.json\nconfig/obsidian-vault/.obsidian/core-plugins.json\nconfig/obsidian-vault/.obsidian/graph.json\nconfig/obsidian-vault/.obsidian/snippets/wide.css\nconfig/obsidian-vault/.obsidian/templates.json\nconfig/obsidian-vault/.obsidian/themes/Tokyo Night/LICENSE\nconfig/obsidian-vault/.obsidian/themes/Tokyo Night/manifest.json\nconfig/obsidian-vault/.obsidian/themes/Tokyo Night/theme.css\nconfig/obsidian-vault/_attachments/.gitkeep\nconfig/obsidian-vault/_templates/.gitkeep\nconfig/repository-files.txt\nconfig/shell.zsh\nconfig/sources.tsv\nconfig/vimrc\nconfig/vscode-extensions.txt\nconfig/vscode-settings.json\nconfig/zsh/development.zsh\nconfig/zsh/framework.zsh\nconfig/zsh/git-functions.zsh\nconfig/zsh/media.zsh\nconfig/zsh/options.zsh\nconfig/zsh/terminal.zsh\nconfig/zsh/tools.zsh\nconfig/zsh/utilities.zsh\ndocs/automation.md\ndocs/battery.md\ndocs/battery.schema.json\ndocs/development.md\ndocs/installation.md\ndocs/maintenance.md\ndocs/obsidian.md\ndocs/result.schema.json\ndocs/shell-editor.md\ndocs/sources.md\ndocs/troubleshooting.md\ndocs/updates.schema.json\nscripts/battery.sh\nscripts/check-change.py\nscripts/check-format.py\nscripts/check-repository.py\nscripts/check-updates.py\nscripts/configure.py\nscripts/desktop.py\nscripts/docker-smoke.sh\nscripts/editor.sh\nscripts/inventory.py\nscripts/kiro-shell.py\nscripts/manual.py\nscripts/ob.py\nscripts/official_ai.py\nscripts/plain-log.awk\nscripts/prepare-doubao.py\nscripts/prepare-sogou.py\nscripts/quality.sh\nscripts/runtime-smoke.py\nscripts/session.sh\nscripts/welcome.sh\nscripts/verify.sh\ntests/fixtures/battery/pmset-log-batt-uppercase.txt\ntests/fixtures/battery/pmset-log-batt.txt\ntests/fixtures/config-golden.json\ntests/test_battery.py\ntests/test_desktop.py\ntests/test_doubao.py\ntests/test_editor.py\ntests/test_interruptions.py\ntests/test_kiro_shell.py\ntests/test_maintenance.py\ntests/test_obsidian.py\ntests/test_official_ai.py\ntests/test_qbittorrent.py\ntests/test_repository.py\ntests/test_session.py\ntests/test_setup.py\ntests/test_sogou.py\ntests/test_welcome.py\n", "shell.zsh": "# Managed by 1bite. Personal overrides belong after the source in .zshrc.\nsource \"${${(%):-%N}:A:h}/env.zsh\"\n[[ -o interactive ]] || return 0\n\nsetup_shell_directory=\"${${(%):-%N}:A:h}\"\nfor setup_shell_module in framework options tools development utilities media git-functions terminal; do\n setup_shell_path=\"$setup_shell_directory/zsh/$setup_shell_module.zsh\"\n [[ -r \"$setup_shell_path\" ]] || { print -u2 \"1bite: missing Zsh module: $setup_shell_path\"; return 1; }\n source \"$setup_shell_path\" || return\ndone\nunset setup_shell_directory setup_shell_module setup_shell_path\n", - "sources.tsv": "homebrew\tinstaller\thttps://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh\nclaude\tinstaller\thttps://claude.ai/install.sh\ncodex\tinstaller\thttps://chatgpt.com/codex/install.sh\nkiro-cli\tinstaller\thttps://cli.kiro.dev/install\nchatgpt\tdocumentation\thttps://learn.chatgpt.com/docs/app\nkiro\tdocumentation\thttps://kiro.dev/downloads/\nchatgpt-download\tdistribution\thttps://persistent.oaistatic.com/codex-app-prod/Codex.dmg\nchatgpt-distribution\tdistribution\thttps://persistent.oaistatic.com/codex-app-prod/\nchatgpt-releases\tmetadata\thttps://persistent.oaistatic.com/codex-app-prod/appcast.xml\nkiro-distribution\tdistribution\thttps://prod.download.desktop.kiro.dev/\nkiro-cli-distribution\tdistribution\thttps://prod.download.cli.kiro.dev/stable/\nkiro-cli-releases\tmetadata\thttps://prod.download.cli.kiro.dev/stable/latest/manifest.json\nclaude-releases\tmetadata\thttps://downloads.claude.ai/claude-code-releases/latest\ncodex-releases\tmetadata\thttps://releases.openai.com/codex/channels/latest\nohmyzsh\tinstaller\thttps://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh\npowerlevel10k\tgit\thttps://github.com/romkatv/powerlevel10k.git\nneofetch\tinstaller\thttps://raw.githubusercontent.com/dylanaraps/neofetch/7.1.0/neofetch\nneofetch-sha256\tchecksum\t3dc33493e54029fb1528251552093a9f9a2894fcf94f9c3a6f809136a42348c7\ndoubao-input-download\tdistribution\thttps://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/\ndoubao-input-metadata\tmetadata\thttps://formulae.brew.sh/api/cask/doubaoime.json\nsogou-download\tdistribution\thttps://ime.gtimg.com/pc/\nhomebrew-api\tconnectivity\thttps://formulae.brew.sh/api/formula/go.json\ngithub\tconnectivity\thttps://github.com\nghcr\tconnectivity\thttps://ghcr.io/v2/\nnpm\tconnectivity\thttps://registry.npmjs.org/\nanthropic\tconnectivity\thttps://claude.ai/install.sh\nopenai\tconnectivity\thttps://chatgpt.com/codex/install.sh\nmaintenance-catalog\tmetadata\thttps://formulae.brew.sh/api/\nmaintenance-aliases\tmetadata\thttps://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Aliases/\nmaintenance-gallery\tmetadata\thttps://marketplace.visualstudio.com/_apis/public/gallery/extensionquery\nmaintenance-homebrew\tmetadata\thttps://api.github.com/repos/Homebrew/brew/releases/latest\nmaintenance-homebrew-release\tmetadata\thttps://github.com/Homebrew/brew/releases/latest\nmaintenance-one-bite\tgit\thttps://github.com/1set/1bite.git\nmaintenance-ohmyzsh\tgit\thttps://github.com/ohmyzsh/ohmyzsh.git\nmaintenance-charm\tmetadata\thttps://raw.githubusercontent.com/charmbracelet/homebrew-tap/master/\ngoogle-chrome-download\tdistribution\thttps://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg\ndocker-desktop-download\tdistribution\thttps://desktop.docker.com/mac/main/arm64/Docker.dmg\nclaude-desktop-releases\tmetadata\thttps://downloads.claude.ai/releases/darwin/universal/RELEASES.json\nclaude-desktop-distribution\tdistribution\thttps://downloads.claude.ai/releases/darwin/universal/\nclaude-desktop-download\tdistribution\thttps://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect\n", + "sources.tsv": "homebrew\tinstaller\thttps://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh\nclaude\tinstaller\thttps://claude.ai/install.sh\ncodex\tinstaller\thttps://chatgpt.com/codex/install.sh\nkiro-cli\tinstaller\thttps://cli.kiro.dev/install\nchatgpt\tdocumentation\thttps://learn.chatgpt.com/docs/app\nkiro\tdocumentation\thttps://kiro.dev/downloads/\nchatgpt-download\tdistribution\thttps://persistent.oaistatic.com/codex-app-prod/Codex.dmg\nchatgpt-distribution\tdistribution\thttps://persistent.oaistatic.com/codex-app-prod/\nchatgpt-releases\tmetadata\thttps://persistent.oaistatic.com/codex-app-prod/appcast.xml\nkiro-distribution\tdistribution\thttps://prod.download.desktop.kiro.dev/\nkiro-releases\tmetadata\thttps://prod.download.desktop.kiro.dev/stable/metadata-dmg-darwin-arm64-stable.json\nkiro-cli-distribution\tdistribution\thttps://prod.download.cli.kiro.dev/stable/\nkiro-cli-releases\tmetadata\thttps://prod.download.cli.kiro.dev/stable/latest/manifest.json\nclaude-releases\tmetadata\thttps://downloads.claude.ai/claude-code-releases/latest\ncodex-releases\tmetadata\thttps://releases.openai.com/codex/channels/latest\nohmyzsh\tinstaller\thttps://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh\npowerlevel10k\tgit\thttps://github.com/romkatv/powerlevel10k.git\nneofetch\tinstaller\thttps://raw.githubusercontent.com/dylanaraps/neofetch/7.1.0/neofetch\nneofetch-sha256\tchecksum\t3dc33493e54029fb1528251552093a9f9a2894fcf94f9c3a6f809136a42348c7\ndoubao-input-download\tdistribution\thttps://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/\ndoubao-input-metadata\tmetadata\thttps://formulae.brew.sh/api/cask/doubaoime.json\nsogou-download\tdistribution\thttps://ime.gtimg.com/pc/\nhomebrew-api\tconnectivity\thttps://formulae.brew.sh/api/formula/go.json\ngithub\tconnectivity\thttps://github.com\nghcr\tconnectivity\thttps://ghcr.io/v2/\nnpm\tconnectivity\thttps://registry.npmjs.org/\nanthropic\tconnectivity\thttps://claude.ai/install.sh\nopenai\tconnectivity\thttps://chatgpt.com/codex/install.sh\nmaintenance-catalog\tmetadata\thttps://formulae.brew.sh/api/\nmaintenance-aliases\tmetadata\thttps://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Aliases/\nmaintenance-gallery\tmetadata\thttps://marketplace.visualstudio.com/_apis/public/gallery/extensionquery\nmaintenance-homebrew\tmetadata\thttps://api.github.com/repos/Homebrew/brew/releases/latest\nmaintenance-homebrew-release\tmetadata\thttps://github.com/Homebrew/brew/releases/latest\nmaintenance-one-bite\tgit\thttps://github.com/1set/1bite.git\nmaintenance-ohmyzsh\tgit\thttps://github.com/ohmyzsh/ohmyzsh.git\nmaintenance-charm\tmetadata\thttps://raw.githubusercontent.com/charmbracelet/homebrew-tap/master/\ngoogle-chrome-download\tdistribution\thttps://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg\ndocker-desktop-download\tdistribution\thttps://desktop.docker.com/mac/main/arm64/Docker.dmg\nclaude-desktop-releases\tmetadata\thttps://downloads.claude.ai/releases/darwin/universal/RELEASES.json\nclaude-desktop-distribution\tdistribution\thttps://downloads.claude.ai/releases/darwin/universal/\nclaude-desktop-download\tdistribution\thttps://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect\n", "vimrc": "\" Managed by 1bite. Personal overrides belong after the source line in ~/.vimrc.\nscriptencoding utf-8\nset encoding=utf-8\n\nset modelines=0\n\nset backspace=2\nset nocompatible\n\nset nobackup\nset nowritebackup\nset noswapfile\nset noundofile\n\nsyntax on\nset number\n\nset ruler\nset smartindent shiftwidth=4\nset tabstop=4\nset expandtab\n\nset list\nset listchars=tab:»·,trail:·,extends:>,precedes:<\n\ncom E e.\n\ncab W! w!\ncab Q! q!\ncab Wq wq\ncab Wa wa\ncab wQ wq\ncab WQ wq\ncab W w\ncab Q q\n", "vscode-extensions.txt": "golang.go\nms-python.python\ncharliermarsh.ruff\ndbaeumer.vscode-eslint\nesbenp.prettier-vscode\nredhat.vscode-yaml\ntimonwong.shellcheck\nms-vscode.sublime-keybindings\n", "vscode-settings.json": "{\n \"terminal.integrated.defaultProfile.osx\": \"zsh\",\n \"terminal.integrated.profiles.osx\": {\n \"zsh\": {\n \"path\": \"/bin/zsh\",\n \"args\": [\"-l\"]\n }\n },\n \"workbench.colorTheme\": \"Default Dark Modern\"\n}\n", diff --git a/tests/test_doubao.py b/tests/test_doubao.py index 975311d..8b493c6 100644 --- a/tests/test_doubao.py +++ b/tests/test_doubao.py @@ -22,6 +22,7 @@ def setUp(self): self.addCleanup(temporary.cleanup) self.directory = Path(temporary.name) / 'packages' self.build = '1000103' + self.release = '20261010_175807_23027789' stream = io.BytesIO() with zipfile.ZipFile(stream, 'w') as archive: root = f'DoubaoImeInstaller_v{self.build}.app' @@ -34,9 +35,9 @@ def setUp(self): self.payload = stream.getvalue() self.item = { 'token': 'doubaoime', - 'version': f'1.0.1,{self.build}', + 'version': f'1.0.1,{self.build},{self.release}', 'url': ('https://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/' - f'DoubaoImeInstaller_v{self.build}_release.zip'), + f'DoubaoImeInstaller_v{self.build}_release_{self.release}.zip'), 'sha256': hashlib.sha256(self.payload).hexdigest(), } @@ -48,14 +49,29 @@ def test_source_contract_rejects_changed_origin_version_and_checksum(self): self.assertEqual(command[0], 'curl') self.assertIn('https://formulae.brew.sh/api/cask/doubaoime.json', command) self.assertIn('--proto', command) + legacy = dict( + self.item, + version=f'1.0.1,{self.build}', + url=('https://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/' + f'DoubaoImeInstaller_v{self.build}_release.zip'), + ) + with patch.object(doubao.subprocess, 'check_output', return_value=json.dumps(legacy)): + self.assertEqual(doubao.metadata(), (legacy, '1.0.1', self.build)) invalid = [ ('token', 'another-app'), ('sha256', 'no_check'), ('version', '1.0.1'), - ('version', '1.0.1,999'), + ('version', f'1.0.1,{self.build}'), + ('version', f'1.0.1,999,{self.release}'), + ('version', f'1.0.1,{self.build},20261010_175807_00000000'), + ('version', f'1.0.1,{self.build},{self.release},extra'), ('url', 'http://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/file.zip'), ('url', 'https://lf-wave.doubaocdn.com.attacker.invalid/obj/doubao-ime/app/macos/file.zip'), ('url', 'https://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/../file.zip'), + ('url', ('https://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/' + f'DoubaoImeInstaller_v{self.build}_release.zip')), + ('url', ('https://lf-wave.doubaocdn.com/obj/doubao-ime/app/macos/' + f'DoubaoImeInstaller_v{self.build}_release_20261010_bad.zip')), ] for field, value in invalid: with self.subTest(field=field, value=value): diff --git a/tests/test_official_ai.py b/tests/test_official_ai.py index 28391f6..c863c8d 100644 --- a/tests/test_official_ai.py +++ b/tests/test_official_ai.py @@ -48,11 +48,33 @@ def feed(version, build, arch='arm64'): xml = '' + feed('1.9.0', 1) + feed('1.10.0', 2) + feed('99.0.0', 3, 'x64') + '' result = ai.release('chatgpt', catalog, lambda _: xml) self.assertEqual((result['version'], result['build'], result['url']), ('1.10.0', '2', catalog['chatgpt-download'])) - urls = [catalog['kiro-distribution'] + f'releases/stable/darwin-arm64/signed/{v}/kiro-ide-{v}-stable-darwin-arm64.dmg' for v in ('1.9.0', '1.10.0')] - self.assertEqual(ai.release('kiro', catalog, lambda _: ' '.join(urls))['version'], '1.10.0') - for text in ('https://example.invalid/kiro.dmg', urls[0].replace('stable', 'preview'), urls[0].replace('arm64', 'x64')): + version = '1.10.0' + url = catalog['kiro-distribution'] + f'releases/stable/darwin-arm64/signed/{version}/kiro-ide-{version}-stable-darwin-arm64.dmg' + kiro = dict(currentRelease=version, + releases=[dict(version=version, updateTo=dict(version=version, url=url))]) + requested = [] + self.assertEqual(ai.release( + 'kiro', catalog, + lambda source: requested.append(source) or json.dumps(kiro))['version'], version) + self.assertEqual(requested, [catalog['kiro-releases']]) + for changed in ( + [], + dict(currentRelease=version, releases={}), + dict(currentRelease='1.10', releases=kiro['releases']), + dict(currentRelease=version, releases=['invalid']), + dict(kiro, currentRelease='1.9.0'), + dict(kiro, releases=[]), + dict(kiro, releases=kiro['releases'] * 2), + dict(kiro, releases=[dict(version=version, updateTo=dict( + version='1.9.0', url=url))]), + dict(kiro, releases=[dict(version=version, updateTo=dict( + version=version, url='https://example.invalid/kiro.dmg'))]), + dict(kiro, releases=[dict(version=version, updateTo=dict( + version=version, url=url.replace('/stable/', '/preview/')))]), + dict(kiro, releases=[dict(version=version, updateTo=dict( + version=version, url=url.replace('arm64', 'x64')))])): with self.assertRaises(ValueError): - ai.release('kiro', catalog, lambda _: text) + ai.release('kiro', catalog, lambda _, value=changed: json.dumps(value)) self.assertEqual(ai.release('codex', catalog, lambda _: '{"tag_name":"rust-v0.200.0"}')['version'], '0.200.0') self.assertEqual(ai.release('claude', catalog, lambda _: '2.5.0\n')['version'], '2.5.0')