From 7497160152d70aec58273dc88463b421a22fe149 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Fri, 6 Feb 2026 08:07:53 +0000 Subject: [PATCH 01/12] fix(spx-gui): filter failed/incomplete tasks in asset-adoption Only include successfully completed tasks when calling /aigc/asset-adoption. This prevents 400 errors when the payload contains failed/incomplete task IDs. - Modified getTaskIds() in animation-gen.ts, costume-gen.ts to check TaskStatus.Completed - Modified recordAdoption() in sprite-gen.ts, backdrop-gen.ts to filter completed tasks Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.ts | 7 +++++-- spx-gui/src/models/gen/backdrop-gen.ts | 5 +++-- spx-gui/src/models/gen/costume-gen.ts | 4 ++-- spx-gui/src/models/gen/sprite-gen.ts | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.ts b/spx-gui/src/models/gen/animation-gen.ts index 5029f8954..980785107 100644 --- a/spx-gui/src/models/gen/animation-gen.ts +++ b/spx-gui/src/models/gen/animation-gen.ts @@ -7,7 +7,8 @@ import { type AnimationSettings, enrichAnimationSettings, type TaskParamsExtractVideoFrames, - TaskType + TaskType, + TaskStatus } from '@/apis/aigc' import type { Project } from '../project' import { Sprite } from '../sprite' @@ -76,7 +77,9 @@ export class AnimationGen extends Disposable { } getTaskIds() { - return [this.generateVideoTask, this.extractFramesTask].map((t) => t.data?.id).filter((id) => id != null) + return [this.generateVideoTask, this.extractFramesTask] + .filter((t) => t.data?.status === TaskStatus.Completed) + .map((t) => t.data!.id) } get name() { diff --git a/spx-gui/src/models/gen/backdrop-gen.ts b/spx-gui/src/models/gen/backdrop-gen.ts index ddddaddf7..141b535e9 100644 --- a/spx-gui/src/models/gen/backdrop-gen.ts +++ b/spx-gui/src/models/gen/backdrop-gen.ts @@ -2,7 +2,7 @@ import { nanoid } from 'nanoid' import { reactive } from 'vue' import { Disposable } from '@/utils/disposable' import { ArtStyle, BackdropCategory, Perspective } from '@/apis/common' -import { adoptAsset, enrichBackdropSettings, TaskType, type BackdropSettings } from '@/apis/aigc' +import { adoptAsset, enrichBackdropSettings, TaskType, TaskStatus, type BackdropSettings } from '@/apis/aigc' import type { File } from '../common/file' import { createFileWithUniversalUrl } from '../common/cloud' import { validateBackdropName } from '../common/asset-name' @@ -104,7 +104,8 @@ export class BackdropGen extends Disposable { async recordAdoption() { const backdrop = this.result if (backdrop == null) throw new Error('result backdrop expected') - const taskIds = this.generateTask.data != null ? [this.generateTask.data.id] : [] + const taskIds = + this.generateTask.data?.status === TaskStatus.Completed ? [this.generateTask.data.id] : [] const assetData = await backdrop2Asset(backdrop) const { name: displayName, description, ...extraSettings } = this.settings return adoptAsset({ diff --git a/spx-gui/src/models/gen/costume-gen.ts b/spx-gui/src/models/gen/costume-gen.ts index f4d82b267..93d2be6c1 100644 --- a/spx-gui/src/models/gen/costume-gen.ts +++ b/spx-gui/src/models/gen/costume-gen.ts @@ -3,7 +3,7 @@ import { reactive } from 'vue' import type { Prettify } from '@/utils/types' import { Disposable } from '@/utils/disposable' import { ArtStyle, Perspective } from '@/apis/common' -import { enrichCostumeSettings, Facing, TaskType, type CostumeSettings } from '@/apis/aigc' +import { enrichCostumeSettings, Facing, TaskType, TaskStatus, type CostumeSettings } from '@/apis/aigc' import type { File } from '../common/file' import { ensureValidCostumeName, validateCostumeName } from '../common/asset-name' import { createFileWithUniversalUrl, saveFile } from '../common/cloud' @@ -62,7 +62,7 @@ export class CostumeGen extends Disposable { } getTaskIds() { - if (this.generateTask.data == null) return [] + if (this.generateTask.data?.status !== TaskStatus.Completed) return [] return [this.generateTask.data.id] } diff --git a/spx-gui/src/models/gen/sprite-gen.ts b/spx-gui/src/models/gen/sprite-gen.ts index 135847982..be298035d 100644 --- a/spx-gui/src/models/gen/sprite-gen.ts +++ b/spx-gui/src/models/gen/sprite-gen.ts @@ -10,6 +10,7 @@ import { type CostumeSettings, Facing, TaskType, + TaskStatus, adoptAsset } from '@/apis/aigc' import { Project } from '../project' @@ -357,7 +358,7 @@ export class SpriteGen extends Disposable { const sprite = this.result if (sprite == null) throw new Error('result sprite expected') const taskIds = [ - this.genImagesTask.data?.id, + this.genImagesTask.data?.status === TaskStatus.Completed ? this.genImagesTask.data.id : null, ...this.costumes.flatMap((c) => c.getTaskIds()), ...this.animations.flatMap((a) => a.getTaskIds()) ].filter((id) => id != null) From c9b116a996e87d1431fe0bd49806e03e0687251c Mon Sep 17 00:00:00 2001 From: xgopilot Date: Fri, 6 Feb 2026 08:22:52 +0000 Subject: [PATCH 02/12] test(spx-gui): add tests for task status filtering in asset adoption Add comprehensive test cases to verify that only completed tasks are included in asset adoption and getTaskIds() methods: - backdrop-gen.test.ts: Test recordAdoption filters by TaskStatus.Completed - costume-gen.test.ts: Test getTaskIds filters by TaskStatus.Completed - animation-gen.test.ts: Test getTaskIds with multiple tasks and mixed statuses - sprite-gen.test.ts: Test recordAdoption with complex scenarios including failed and cancelled tasks These tests ensure that the fix for #2824 works correctly by preventing HTTP 400 errors when failed/incomplete tasks are present. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/package-lock.json | 35 +------- spx-gui/src/models/gen/animation-gen.test.ts | 79 ++++++++++++++++++ spx-gui/src/models/gen/backdrop-gen.test.ts | 55 ++++++++++++- spx-gui/src/models/gen/costume-gen.test.ts | 34 ++++++++ spx-gui/src/models/gen/sprite-gen.test.ts | 84 ++++++++++++++++++++ 5 files changed, 254 insertions(+), 33 deletions(-) diff --git a/spx-gui/package-lock.json b/spx-gui/package-lock.json index 1cc7cce1a..5a037d776 100644 --- a/spx-gui/package-lock.json +++ b/spx-gui/package-lock.json @@ -109,7 +109,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -662,7 +661,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -699,7 +697,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -709,7 +706,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -2615,7 +2611,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3386,7 +3381,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -4291,7 +4285,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4478,8 +4471,7 @@ "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "peer": true + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" }, "node_modules/bail": { "version": "2.0.2", @@ -4597,7 +4589,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5032,7 +5023,6 @@ "version": "0.15.12", "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.12.tgz", "integrity": "sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==", - "peer": true, "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" @@ -5089,7 +5079,6 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -5477,7 +5466,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5537,7 +5525,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5617,7 +5604,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.2.tgz", "integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -5876,7 +5862,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6132,7 +6117,6 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.3.0" } @@ -6202,7 +6186,6 @@ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=10" } @@ -7280,7 +7263,6 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -7316,8 +7298,7 @@ "type": "github", "url": "https://github.com/sponsors/lavrton" } - ], - "peer": true + ] }, "node_modules/leven": { "version": "4.1.0", @@ -8924,7 +8905,6 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9409,7 +9389,6 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9525,7 +9504,6 @@ "version": "1.70.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -9812,8 +9790,7 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/source-map-js": { "version": "1.2.1", @@ -10164,7 +10141,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "peer": true, "engines": { "node": ">=12" }, @@ -10313,7 +10289,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10624,7 +10599,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -11297,7 +11271,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "peer": true, "engines": { "node": ">=12" }, @@ -11433,7 +11406,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -11803,7 +11775,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 0c184aa07..86d144cd4 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -296,4 +296,83 @@ describe('AnimationGen', () => { const lastRecord = Array.from(tasks.values()).at(-1) expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) + + it('should only return completed task IDs in getTaskIds', async () => { + const project = makeProject() + const sprite = Sprite.create('TestSprite', '') + const defaultCostume = new Costume('default', mockFile()) + sprite.addCostume(defaultCostume) + project.addSprite(sprite) + const gen = new AnimationGen(sprite, project, { + description: 'A test animation', + referenceCostumeId: defaultCostume.id + }) + + await gen.enrich() + await gen.generateVideo() + gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) + await gen.extractFrames() + + // Both tasks should be completed + const taskIds = gen.getTaskIds() + expect(taskIds).toHaveLength(2) + expect(taskIds).toContain(gen.generateVideoTask.data?.id) + expect(taskIds).toContain(gen.extractFramesTask.data?.id) + }) + + it('should exclude non-completed task IDs from getTaskIds', async () => { + const project = makeProject() + const sprite = Sprite.create('TestSprite', '') + const defaultCostume = new Costume('default', mockFile()) + sprite.addCostume(defaultCostume) + project.addSprite(sprite) + const gen = new AnimationGen(sprite, project, { + description: 'A test animation', + referenceCostumeId: defaultCostume.id + }) + + await gen.enrich() + await gen.generateVideo() + gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) + await gen.extractFrames() + + // Manually modify task statuses to simulate failures + if (gen.generateVideoTask.data) { + gen.generateVideoTask.data.status = TaskStatus.Failed + } + if (gen.extractFramesTask.data) { + gen.extractFramesTask.data.status = TaskStatus.Cancelled + } + + // getTaskIds should return empty array since both tasks are not completed + const taskIds = gen.getTaskIds() + expect(taskIds).toHaveLength(0) + }) + + it('should handle mixed task statuses in getTaskIds', async () => { + const project = makeProject() + const sprite = Sprite.create('TestSprite', '') + const defaultCostume = new Costume('default', mockFile()) + sprite.addCostume(defaultCostume) + project.addSprite(sprite) + const gen = new AnimationGen(sprite, project, { + description: 'A test animation', + referenceCostumeId: defaultCostume.id + }) + + await gen.enrich() + await gen.generateVideo() + gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) + await gen.extractFrames() + + // Manually modify one task to fail, keep the other completed + if (gen.generateVideoTask.data) { + gen.generateVideoTask.data.status = TaskStatus.Failed + } + + // getTaskIds should only return the completed extractFramesTask + const taskIds = gen.getTaskIds() + expect(taskIds).toHaveLength(1) + expect(taskIds[0]).toBe(gen.extractFramesTask.data?.id) + }) }) diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index fec51baf0..a765feb84 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' import { ArtStyle, BackdropCategory, Perspective } from '@/apis/common' import { TaskStatus } from '@/apis/aigc' +import * as aigcApis from '@/apis/aigc' import { makeProject } from '../common/test' import { setupAigcMock, MockAigcApis } from './aigc-mock' import { BackdropGen } from './backdrop-gen' @@ -205,4 +206,56 @@ describe('BackdropGen', () => { const lastRecord = Array.from(tasks.values()).at(-1) expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) + + it('should only include completed task IDs in recordAdoption', async () => { + const project = makeProject() + const gen = new BackdropGen(project, 'A test backdrop') + + await gen.enrich() + await gen.genImages() + gen.setImage(gen.imagesGenState.result![0]) + await gen.finish() + + // Mock adoptAsset to inspect the taskIds parameter + const adoptAssetCalls: unknown[] = [] + vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { + adoptAssetCalls.push(params) + }) + + await gen.recordAdoption() + + // Verify that taskIds contains the task ID (task should be completed) + expect(adoptAssetCalls).toHaveLength(1) + const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } + expect(adoptParams.taskIds).toHaveLength(1) + expect(adoptParams.taskIds[0]).toBe(gen.generateTask.data?.id) + }) + + it('should exclude non-completed task IDs from recordAdoption', async () => { + const project = makeProject() + const gen = new BackdropGen(project, 'A test backdrop') + + await gen.enrich() + await gen.genImages() + gen.setImage(gen.imagesGenState.result![0]) + await gen.finish() + + // Manually modify the task status to simulate a failed task + if (gen.generateTask.data) { + gen.generateTask.data.status = TaskStatus.Failed + } + + // Mock adoptAsset to inspect the taskIds parameter + const adoptAssetCalls: unknown[] = [] + vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { + adoptAssetCalls.push(params) + }) + + await gen.recordAdoption() + + // Verify that taskIds is empty since the task is failed + expect(adoptAssetCalls).toHaveLength(1) + const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } + expect(adoptParams.taskIds).toHaveLength(0) + }) }) diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index d5f3a76a8..633fb8c0f 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -184,4 +184,38 @@ describe('CostumeGen', () => { const lastRecord = Array.from(tasks.values()).at(-1) expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) + + it('should only return completed task IDs in getTaskIds', async () => { + const project = makeProject() + const sprite = Sprite.create('TestSprite', '') + project.addSprite(sprite) + const gen = new CostumeGen(sprite, project, { description: 'A test costume' }) + + await gen.enrich() + await gen.generate() + + // Task should be completed, so getTaskIds should return it + const taskIds = gen.getTaskIds() + expect(taskIds).toHaveLength(1) + expect(taskIds[0]).toBe(gen.generateTask.data?.id) + }) + + it('should exclude non-completed task IDs from getTaskIds', async () => { + const project = makeProject() + const sprite = Sprite.create('TestSprite', '') + project.addSprite(sprite) + const gen = new CostumeGen(sprite, project, { description: 'A test costume' }) + + await gen.enrich() + await gen.generate() + + // Manually modify the task status to simulate a failed task + if (gen.generateTask.data) { + gen.generateTask.data.status = TaskStatus.Failed + } + + // getTaskIds should return empty array since task is failed + const taskIds = gen.getTaskIds() + expect(taskIds).toHaveLength(0) + }) }) diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index a7cc6bb7d..3464a4a66 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { ArtStyle, Perspective, SpriteCategory } from '@/apis/common' import { TaskStatus } from '@/apis/aigc' +import * as aigcApis from '@/apis/aigc' import { createI18n } from '@/utils/i18n' import * as fileHelpers from '@/models/common/file' import { makeProject } from '../common/test' @@ -320,4 +321,87 @@ describe('SpriteGen', () => { const sprite3 = gen3.finish() expect(sprite3.rotationStyle).toBe(RotationStyle.Normal) }) + + it('should only include completed task IDs in recordAdoption', async () => { + const project = makeProject() + const gen = new SpriteGen(createI18n({ lang: 'en' }), project, 'A test sprite') + + await gen.enrich() + await gen.genImages() + gen.setImage(gen.imagesGenState.result![0]) + await gen.prepareContent() + + // Finish all sub-generations + for (const costumeGen of gen.costumes) { + await finishCostumeGen(costumeGen.name, costumeGen) + } + for (const animationGen of gen.animations) { + await finishAnimationGen(animationGen.name, animationGen) + } + + const sprite = gen.finish() + project.addSprite(sprite) + + // Mock adoptAsset to inspect the taskIds parameter + const adoptAssetCalls: unknown[] = [] + vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { + adoptAssetCalls.push(params) + }) + + await gen.recordAdoption() + + // Verify that taskIds contains all completed task IDs + expect(adoptAssetCalls).toHaveLength(1) + const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } + // Should include: genImagesTask + costume tasks + animation tasks (generateVideo + extractFrames) + const expectedCount = 1 + gen.costumes.length + gen.animations.length * 2 + expect(adoptParams.taskIds.length).toBeGreaterThanOrEqual(expectedCount) + }) + + it('should exclude failed/cancelled task IDs from recordAdoption', async () => { + const project = makeProject() + const gen = new SpriteGen(createI18n({ lang: 'en' }), project, 'A test sprite') + + await gen.enrich() + await gen.genImages() + gen.setImage(gen.imagesGenState.result![0]) + await gen.prepareContent() + + // Finish all sub-generations + for (const costumeGen of gen.costumes) { + await finishCostumeGen(costumeGen.name, costumeGen) + } + for (const animationGen of gen.animations) { + await finishAnimationGen(animationGen.name, animationGen) + } + + const sprite = gen.finish() + project.addSprite(sprite) + + // Manually modify task statuses to simulate failures + if (gen.genImagesTask.data) { + gen.genImagesTask.data.status = TaskStatus.Failed + } + if (gen.costumes[0]?.generateTask.data) { + gen.costumes[0].generateTask.data.status = TaskStatus.Cancelled + } + if (gen.animations[0]?.generateVideoTask.data) { + gen.animations[0].generateVideoTask.data.status = TaskStatus.Failed + } + + // Mock adoptAsset to inspect the taskIds parameter + const adoptAssetCalls: unknown[] = [] + vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { + adoptAssetCalls.push(params) + }) + + await gen.recordAdoption() + + // Verify that failed/cancelled tasks are excluded + expect(adoptAssetCalls).toHaveLength(1) + const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } + expect(adoptParams.taskIds).not.toContain(gen.genImagesTask.data?.id) + expect(adoptParams.taskIds).not.toContain(gen.costumes[0]?.generateTask.data?.id) + expect(adoptParams.taskIds).not.toContain(gen.animations[0]?.generateVideoTask.data?.id) + }) }) From 03122d80ec53ee028ebc75efe32709b4999d23ad Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:09:00 +0000 Subject: [PATCH 03/12] chore: revert package-lock.json changes Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/package-lock.json | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/spx-gui/package-lock.json b/spx-gui/package-lock.json index 5a037d776..1cc7cce1a 100644 --- a/spx-gui/package-lock.json +++ b/spx-gui/package-lock.json @@ -109,6 +109,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -661,6 +662,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -697,6 +699,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -706,6 +709,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -2611,6 +2615,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3381,6 +3386,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -4285,6 +4291,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4471,7 +4478,8 @@ "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "peer": true }, "node_modules/bail": { "version": "2.0.2", @@ -4589,6 +4597,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5023,6 +5032,7 @@ "version": "0.15.12", "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.12.tgz", "integrity": "sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==", + "peer": true, "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" @@ -5079,6 +5089,7 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -5466,6 +5477,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5525,6 +5537,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5604,6 +5617,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.2.tgz", "integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -5862,6 +5876,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6117,6 +6132,7 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.3.0" } @@ -6186,6 +6202,7 @@ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=10" } @@ -7263,6 +7280,7 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -7298,7 +7316,8 @@ "type": "github", "url": "https://github.com/sponsors/lavrton" } - ] + ], + "peer": true }, "node_modules/leven": { "version": "4.1.0", @@ -8905,6 +8924,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9389,6 +9409,7 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9504,6 +9525,7 @@ "version": "1.70.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -9790,7 +9812,8 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/source-map-js": { "version": "1.2.1", @@ -10141,6 +10164,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "peer": true, "engines": { "node": ">=12" }, @@ -10289,6 +10313,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10599,6 +10624,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -11271,6 +11297,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "peer": true, "engines": { "node": ">=12" }, @@ -11406,6 +11433,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -11775,6 +11803,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 7d68ab37b0c4f1542587de8be5bc1852e9f2d2b0 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:10:40 +0000 Subject: [PATCH 04/12] docs(spx-gui): add comments to getTaskIds methods explaining filtering behavior Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.ts | 1 + spx-gui/src/models/gen/costume-gen.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/spx-gui/src/models/gen/animation-gen.ts b/spx-gui/src/models/gen/animation-gen.ts index 980785107..c16f41bcb 100644 --- a/spx-gui/src/models/gen/animation-gen.ts +++ b/spx-gui/src/models/gen/animation-gen.ts @@ -76,6 +76,7 @@ export class AnimationGen extends Disposable { return reactive(this) as this } + /** Get task IDs for completed tasks only. Only completed task IDs will be returned. */ getTaskIds() { return [this.generateVideoTask, this.extractFramesTask] .filter((t) => t.data?.status === TaskStatus.Completed) diff --git a/spx-gui/src/models/gen/costume-gen.ts b/spx-gui/src/models/gen/costume-gen.ts index 93d2be6c1..e7a805504 100644 --- a/spx-gui/src/models/gen/costume-gen.ts +++ b/spx-gui/src/models/gen/costume-gen.ts @@ -61,6 +61,7 @@ export class CostumeGen extends Disposable { return reactive(this) as this } + /** Get task IDs for completed tasks only. Only completed task IDs will be returned. */ getTaskIds() { if (this.generateTask.data?.status !== TaskStatus.Completed) return [] return [this.generateTask.data.id] From 8d699beef601fc4c379b2c6b648bff33d85516a5 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:22:50 +0000 Subject: [PATCH 05/12] fix(spx-gui): refactor tests to avoid accessing private fields - Add helper methods to MockAigcApis (setTaskStatus, getLastTask, getTasksByType) - Update all test files to use MockAigcApis instead of accessing private task fields - This fixes TypeScript errors in CI where tests were accessing private properties Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/aigc-mock.ts | 30 ++++++++++++++++++++ spx-gui/src/models/gen/animation-gen.test.ts | 30 +++++++++++++------- spx-gui/src/models/gen/backdrop-gen.test.ts | 10 ++++--- spx-gui/src/models/gen/costume-gen.test.ts | 8 ++++-- spx-gui/src/models/gen/sprite-gen.test.ts | 28 +++++++++++------- 5 files changed, 79 insertions(+), 27 deletions(-) diff --git a/spx-gui/src/models/gen/aigc-mock.ts b/spx-gui/src/models/gen/aigc-mock.ts index d0595fd34..d394ae879 100644 --- a/spx-gui/src/models/gen/aigc-mock.ts +++ b/spx-gui/src/models/gen/aigc-mock.ts @@ -70,6 +70,36 @@ export class MockAigcApis { } } + /** + * Set the status of a task by its ID. + * This is useful for testing scenarios where tasks fail or are cancelled. + */ + setTaskStatus(taskId: string, status: TaskStatus) { + const record = this.tasks.get(taskId) + if (record == null) throw new Error(`unknown task id: ${taskId}`) + record.task.status = status + record.task.updatedAt = this.now() + } + + /** + * Get the last created task. + * This is useful for tests that need to inspect or manipulate the most recently created task. + */ + getLastTask(): Task | null { + const tasks = Array.from(this.tasks.values()) + return tasks.length > 0 ? tasks[tasks.length - 1].task : null + } + + /** + * Get tasks by type. + * This is useful for tests that need to find specific tasks. + */ + getTasksByType(type: T): Array> { + return Array.from(this.tasks.values()) + .filter((record) => record.task.type === type) + .map((record) => record.task as Task) + } + injectErrorOnce(methodName: MockableMethod, error: Error) { this.errorInjections.set(methodName, error) } diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 86d144cd4..f390c868e 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { AnimationLoopMode, ArtStyle, Perspective } from '@/apis/common' -import { TaskStatus } from '@/apis/aigc' +import { TaskStatus, TaskType } from '@/apis/aigc' import * as fileHelpers from '@/models/common/file' import { makeProject, mockFile } from '../common/test' import { setupAigcMock, MockAigcApis } from './aigc-mock' @@ -314,10 +314,12 @@ describe('AnimationGen', () => { await gen.extractFrames() // Both tasks should be completed + const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) + const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(2) - expect(taskIds).toContain(gen.generateVideoTask.data?.id) - expect(taskIds).toContain(gen.extractFramesTask.data?.id) + expect(taskIds).toContain(generateVideoTasks[generateVideoTasks.length - 1]?.id) + expect(taskIds).toContain(extractFramesTasks[extractFramesTasks.length - 1]?.id) }) it('should exclude non-completed task IDs from getTaskIds', async () => { @@ -337,11 +339,15 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify task statuses to simulate failures - if (gen.generateVideoTask.data) { - gen.generateVideoTask.data.status = TaskStatus.Failed + const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) + const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) + const generateVideoTask = generateVideoTasks[generateVideoTasks.length - 1] + const extractFramesTask = extractFramesTasks[extractFramesTasks.length - 1] + if (generateVideoTask) { + aigcMock.setTaskStatus(generateVideoTask.id, TaskStatus.Failed) } - if (gen.extractFramesTask.data) { - gen.extractFramesTask.data.status = TaskStatus.Cancelled + if (extractFramesTask) { + aigcMock.setTaskStatus(extractFramesTask.id, TaskStatus.Cancelled) } // getTaskIds should return empty array since both tasks are not completed @@ -366,13 +372,17 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify one task to fail, keep the other completed - if (gen.generateVideoTask.data) { - gen.generateVideoTask.data.status = TaskStatus.Failed + const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) + const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) + const generateVideoTask = generateVideoTasks[generateVideoTasks.length - 1] + const extractFramesTask = extractFramesTasks[extractFramesTasks.length - 1] + if (generateVideoTask) { + aigcMock.setTaskStatus(generateVideoTask.id, TaskStatus.Failed) } // getTaskIds should only return the completed extractFramesTask const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(gen.extractFramesTask.data?.id) + expect(taskIds[0]).toBe(extractFramesTask?.id) }) }) diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index a765feb84..d1093a743 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { ArtStyle, BackdropCategory, Perspective } from '@/apis/common' -import { TaskStatus } from '@/apis/aigc' +import { TaskStatus, TaskType } from '@/apis/aigc' import * as aigcApis from '@/apis/aigc' import { makeProject } from '../common/test' import { setupAigcMock, MockAigcApis } from './aigc-mock' @@ -227,8 +227,9 @@ describe('BackdropGen', () => { // Verify that taskIds contains the task ID (task should be completed) expect(adoptAssetCalls).toHaveLength(1) const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } + const task = aigcMock.getTasksByType(TaskType.GenerateBackdrop).pop() expect(adoptParams.taskIds).toHaveLength(1) - expect(adoptParams.taskIds[0]).toBe(gen.generateTask.data?.id) + expect(adoptParams.taskIds[0]).toBe(task?.id) }) it('should exclude non-completed task IDs from recordAdoption', async () => { @@ -241,8 +242,9 @@ describe('BackdropGen', () => { await gen.finish() // Manually modify the task status to simulate a failed task - if (gen.generateTask.data) { - gen.generateTask.data.status = TaskStatus.Failed + const task = aigcMock.getTasksByType(TaskType.GenerateBackdrop).pop() + if (task) { + aigcMock.setTaskStatus(task.id, TaskStatus.Failed) } // Mock adoptAsset to inspect the taskIds parameter diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index 633fb8c0f..a5ff89d5b 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -195,9 +195,10 @@ describe('CostumeGen', () => { await gen.generate() // Task should be completed, so getTaskIds should return it + const task = aigcMock.getLastTask() const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(gen.generateTask.data?.id) + expect(taskIds[0]).toBe(task?.id) }) it('should exclude non-completed task IDs from getTaskIds', async () => { @@ -210,8 +211,9 @@ describe('CostumeGen', () => { await gen.generate() // Manually modify the task status to simulate a failed task - if (gen.generateTask.data) { - gen.generateTask.data.status = TaskStatus.Failed + const task = aigcMock.getLastTask() + if (task) { + aigcMock.setTaskStatus(task.id, TaskStatus.Failed) } // getTaskIds should return empty array since task is failed diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index 3464a4a66..0b2fe6ebe 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { ArtStyle, Perspective, SpriteCategory } from '@/apis/common' -import { TaskStatus } from '@/apis/aigc' +import { TaskStatus, TaskType } from '@/apis/aigc' import * as aigcApis from '@/apis/aigc' import { createI18n } from '@/utils/i18n' import * as fileHelpers from '@/models/common/file' @@ -378,15 +378,23 @@ describe('SpriteGen', () => { const sprite = gen.finish() project.addSprite(sprite) + // Get tasks that we'll mark as failed + // The order is: genImagesTask, then costume tasks, then animation tasks + const allCostumeTasks = aigcMock.getTasksByType(TaskType.GenerateCostume) + const allAnimationVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) + // Manually modify task statuses to simulate failures - if (gen.genImagesTask.data) { - gen.genImagesTask.data.status = TaskStatus.Failed + // Mark the first sprite genImagesTask as failed (it's the first GenerateCostume task) + if (allCostumeTasks[0]) { + aigcMock.setTaskStatus(allCostumeTasks[0].id, TaskStatus.Failed) } - if (gen.costumes[0]?.generateTask.data) { - gen.costumes[0].generateTask.data.status = TaskStatus.Cancelled + // Mark the first costume's generateTask as cancelled (it's the second GenerateCostume task) + if (allCostumeTasks[1]) { + aigcMock.setTaskStatus(allCostumeTasks[1].id, TaskStatus.Cancelled) } - if (gen.animations[0]?.generateVideoTask.data) { - gen.animations[0].generateVideoTask.data.status = TaskStatus.Failed + // Mark the first animation's generateVideoTask as failed + if (allAnimationVideoTasks[0]) { + aigcMock.setTaskStatus(allAnimationVideoTasks[0].id, TaskStatus.Failed) } // Mock adoptAsset to inspect the taskIds parameter @@ -400,8 +408,8 @@ describe('SpriteGen', () => { // Verify that failed/cancelled tasks are excluded expect(adoptAssetCalls).toHaveLength(1) const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - expect(adoptParams.taskIds).not.toContain(gen.genImagesTask.data?.id) - expect(adoptParams.taskIds).not.toContain(gen.costumes[0]?.generateTask.data?.id) - expect(adoptParams.taskIds).not.toContain(gen.animations[0]?.generateVideoTask.data?.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]?.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]?.id) + expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]?.id) }) }) From 4ec26884d7de4db322703038d897f59e75a2157d Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:45:28 +0000 Subject: [PATCH 06/12] chore(spx-gui): fix prettier formatting in backdrop-gen.ts Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/backdrop-gen.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spx-gui/src/models/gen/backdrop-gen.ts b/spx-gui/src/models/gen/backdrop-gen.ts index 141b535e9..e4bdde06f 100644 --- a/spx-gui/src/models/gen/backdrop-gen.ts +++ b/spx-gui/src/models/gen/backdrop-gen.ts @@ -104,8 +104,7 @@ export class BackdropGen extends Disposable { async recordAdoption() { const backdrop = this.result if (backdrop == null) throw new Error('result backdrop expected') - const taskIds = - this.generateTask.data?.status === TaskStatus.Completed ? [this.generateTask.data.id] : [] + const taskIds = this.generateTask.data?.status === TaskStatus.Completed ? [this.generateTask.data.id] : [] const assetData = await backdrop2Asset(backdrop) const { name: displayName, description, ...extraSettings } = this.settings return adoptAsset({ From ed51446630b8228036d9be909b12207df4953308 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:50:12 +0000 Subject: [PATCH 07/12] refactor(spx-gui): simplify MockAigcApis API by exposing tasks directly Remove helper methods (setTaskStatus, getLastTask, getTasksByType) from MockAigcApis and let test code access the tasks Map directly. This provides a simpler, more flexible API where tests can implement their own filtering and manipulation logic. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/aigc-mock.ts | 30 ------------- spx-gui/src/models/gen/animation-gen.test.ts | 45 ++++++++++++-------- spx-gui/src/models/gen/backdrop-gen.test.ts | 14 ++++-- spx-gui/src/models/gen/costume-gen.test.ts | 11 +++-- spx-gui/src/models/gen/sprite-gen.test.ts | 21 +++++---- 5 files changed, 58 insertions(+), 63 deletions(-) diff --git a/spx-gui/src/models/gen/aigc-mock.ts b/spx-gui/src/models/gen/aigc-mock.ts index d394ae879..d0595fd34 100644 --- a/spx-gui/src/models/gen/aigc-mock.ts +++ b/spx-gui/src/models/gen/aigc-mock.ts @@ -70,36 +70,6 @@ export class MockAigcApis { } } - /** - * Set the status of a task by its ID. - * This is useful for testing scenarios where tasks fail or are cancelled. - */ - setTaskStatus(taskId: string, status: TaskStatus) { - const record = this.tasks.get(taskId) - if (record == null) throw new Error(`unknown task id: ${taskId}`) - record.task.status = status - record.task.updatedAt = this.now() - } - - /** - * Get the last created task. - * This is useful for tests that need to inspect or manipulate the most recently created task. - */ - getLastTask(): Task | null { - const tasks = Array.from(this.tasks.values()) - return tasks.length > 0 ? tasks[tasks.length - 1].task : null - } - - /** - * Get tasks by type. - * This is useful for tests that need to find specific tasks. - */ - getTasksByType(type: T): Array> { - return Array.from(this.tasks.values()) - .filter((record) => record.task.type === type) - .map((record) => record.task as Task) - } - injectErrorOnce(methodName: MockableMethod, error: Error) { this.errorInjections.set(methodName, error) } diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index f390c868e..292c8557e 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -314,8 +314,12 @@ describe('AnimationGen', () => { await gen.extractFrames() // Both tasks should be completed - const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) - const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) + const generateVideoTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) + .map((record) => record.task) + const extractFramesTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.ExtractVideoFrames) + .map((record) => record.task) const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(2) expect(taskIds).toContain(generateVideoTasks[generateVideoTasks.length - 1]?.id) @@ -339,15 +343,19 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify task statuses to simulate failures - const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) - const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) - const generateVideoTask = generateVideoTasks[generateVideoTasks.length - 1] - const extractFramesTask = extractFramesTasks[extractFramesTasks.length - 1] - if (generateVideoTask) { - aigcMock.setTaskStatus(generateVideoTask.id, TaskStatus.Failed) + const generateVideoTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) + const extractFramesTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.ExtractVideoFrames) + const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1] + const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1] + if (generateVideoRecord) { + generateVideoRecord.task.status = TaskStatus.Failed + generateVideoRecord.task.updatedAt = new Date().toISOString() } - if (extractFramesTask) { - aigcMock.setTaskStatus(extractFramesTask.id, TaskStatus.Cancelled) + if (extractFramesRecord) { + extractFramesRecord.task.status = TaskStatus.Cancelled + extractFramesRecord.task.updatedAt = new Date().toISOString() } // getTaskIds should return empty array since both tasks are not completed @@ -372,17 +380,20 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify one task to fail, keep the other completed - const generateVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) - const extractFramesTasks = aigcMock.getTasksByType(TaskType.ExtractVideoFrames) - const generateVideoTask = generateVideoTasks[generateVideoTasks.length - 1] - const extractFramesTask = extractFramesTasks[extractFramesTasks.length - 1] - if (generateVideoTask) { - aigcMock.setTaskStatus(generateVideoTask.id, TaskStatus.Failed) + const generateVideoTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) + const extractFramesTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.ExtractVideoFrames) + const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1] + const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1] + if (generateVideoRecord) { + generateVideoRecord.task.status = TaskStatus.Failed + generateVideoRecord.task.updatedAt = new Date().toISOString() } // getTaskIds should only return the completed extractFramesTask const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(extractFramesTask?.id) + expect(taskIds[0]).toBe(extractFramesRecord?.task.id) }) }) diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index d1093a743..37c748aca 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -227,7 +227,10 @@ describe('BackdropGen', () => { // Verify that taskIds contains the task ID (task should be completed) expect(adoptAssetCalls).toHaveLength(1) const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - const task = aigcMock.getTasksByType(TaskType.GenerateBackdrop).pop() + const tasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateBackdrop) + .map((record) => record.task) + const task = tasks[tasks.length - 1] expect(adoptParams.taskIds).toHaveLength(1) expect(adoptParams.taskIds[0]).toBe(task?.id) }) @@ -242,9 +245,12 @@ describe('BackdropGen', () => { await gen.finish() // Manually modify the task status to simulate a failed task - const task = aigcMock.getTasksByType(TaskType.GenerateBackdrop).pop() - if (task) { - aigcMock.setTaskStatus(task.id, TaskStatus.Failed) + const taskRecords = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateBackdrop) + const taskRecord = taskRecords[taskRecords.length - 1] + if (taskRecord) { + taskRecord.task.status = TaskStatus.Failed + taskRecord.task.updatedAt = new Date().toISOString() } // Mock adoptAsset to inspect the taskIds parameter diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index a5ff89d5b..e63168540 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -195,7 +195,8 @@ describe('CostumeGen', () => { await gen.generate() // Task should be completed, so getTaskIds should return it - const task = aigcMock.getLastTask() + const tasks = Array.from(aigcMock.tasks.values()) + const task = tasks[tasks.length - 1]?.task const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) expect(taskIds[0]).toBe(task?.id) @@ -211,9 +212,11 @@ describe('CostumeGen', () => { await gen.generate() // Manually modify the task status to simulate a failed task - const task = aigcMock.getLastTask() - if (task) { - aigcMock.setTaskStatus(task.id, TaskStatus.Failed) + const tasks = Array.from(aigcMock.tasks.values()) + const taskRecord = tasks[tasks.length - 1] + if (taskRecord) { + taskRecord.task.status = TaskStatus.Failed + taskRecord.task.updatedAt = new Date().toISOString() } // getTaskIds should return empty array since task is failed diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index 0b2fe6ebe..49c94c8e3 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -380,21 +380,26 @@ describe('SpriteGen', () => { // Get tasks that we'll mark as failed // The order is: genImagesTask, then costume tasks, then animation tasks - const allCostumeTasks = aigcMock.getTasksByType(TaskType.GenerateCostume) - const allAnimationVideoTasks = aigcMock.getTasksByType(TaskType.GenerateAnimationVideo) + const allCostumeTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateCostume) + const allAnimationVideoTasks = Array.from(aigcMock.tasks.values()) + .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) // Manually modify task statuses to simulate failures // Mark the first sprite genImagesTask as failed (it's the first GenerateCostume task) if (allCostumeTasks[0]) { - aigcMock.setTaskStatus(allCostumeTasks[0].id, TaskStatus.Failed) + allCostumeTasks[0].task.status = TaskStatus.Failed + allCostumeTasks[0].task.updatedAt = new Date().toISOString() } // Mark the first costume's generateTask as cancelled (it's the second GenerateCostume task) if (allCostumeTasks[1]) { - aigcMock.setTaskStatus(allCostumeTasks[1].id, TaskStatus.Cancelled) + allCostumeTasks[1].task.status = TaskStatus.Cancelled + allCostumeTasks[1].task.updatedAt = new Date().toISOString() } // Mark the first animation's generateVideoTask as failed if (allAnimationVideoTasks[0]) { - aigcMock.setTaskStatus(allAnimationVideoTasks[0].id, TaskStatus.Failed) + allAnimationVideoTasks[0].task.status = TaskStatus.Failed + allAnimationVideoTasks[0].task.updatedAt = new Date().toISOString() } // Mock adoptAsset to inspect the taskIds parameter @@ -408,8 +413,8 @@ describe('SpriteGen', () => { // Verify that failed/cancelled tasks are excluded expect(adoptAssetCalls).toHaveLength(1) const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]?.id) - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]?.id) - expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]?.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]?.task.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]?.task.id) + expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]?.task.id) }) }) From 1da69be2f45926ec2e1e7ebcc76f178d440dec14 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:55:00 +0000 Subject: [PATCH 08/12] refactor(spx-gui): use non-null assertions in tests for guaranteed values Replace optional chaining (?.) and if checks with non-null assertions (!) in test cases where values are known to exist. This makes the test code more assertive and cleaner. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.test.ts | 32 ++++++++------------ spx-gui/src/models/gen/backdrop-gen.test.ts | 12 +++----- spx-gui/src/models/gen/costume-gen.test.ts | 12 +++----- spx-gui/src/models/gen/sprite-gen.test.ts | 24 ++++++--------- 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 292c8557e..37f7140e1 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -322,8 +322,8 @@ describe('AnimationGen', () => { .map((record) => record.task) const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(2) - expect(taskIds).toContain(generateVideoTasks[generateVideoTasks.length - 1]?.id) - expect(taskIds).toContain(extractFramesTasks[extractFramesTasks.length - 1]?.id) + expect(taskIds).toContain(generateVideoTasks[generateVideoTasks.length - 1]!.id) + expect(taskIds).toContain(extractFramesTasks[extractFramesTasks.length - 1]!.id) }) it('should exclude non-completed task IDs from getTaskIds', async () => { @@ -347,16 +347,12 @@ describe('AnimationGen', () => { .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) const extractFramesTasks = Array.from(aigcMock.tasks.values()) .filter((record) => record.task.type === TaskType.ExtractVideoFrames) - const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1] - const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1] - if (generateVideoRecord) { - generateVideoRecord.task.status = TaskStatus.Failed - generateVideoRecord.task.updatedAt = new Date().toISOString() - } - if (extractFramesRecord) { - extractFramesRecord.task.status = TaskStatus.Cancelled - extractFramesRecord.task.updatedAt = new Date().toISOString() - } + const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! + const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! + generateVideoRecord.task.status = TaskStatus.Failed + generateVideoRecord.task.updatedAt = new Date().toISOString() + extractFramesRecord.task.status = TaskStatus.Cancelled + extractFramesRecord.task.updatedAt = new Date().toISOString() // getTaskIds should return empty array since both tasks are not completed const taskIds = gen.getTaskIds() @@ -384,16 +380,14 @@ describe('AnimationGen', () => { .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) const extractFramesTasks = Array.from(aigcMock.tasks.values()) .filter((record) => record.task.type === TaskType.ExtractVideoFrames) - const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1] - const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1] - if (generateVideoRecord) { - generateVideoRecord.task.status = TaskStatus.Failed - generateVideoRecord.task.updatedAt = new Date().toISOString() - } + const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! + const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! + generateVideoRecord.task.status = TaskStatus.Failed + generateVideoRecord.task.updatedAt = new Date().toISOString() // getTaskIds should only return the completed extractFramesTask const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(extractFramesRecord?.task.id) + expect(taskIds[0]).toBe(extractFramesRecord.task.id) }) }) diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index 37c748aca..5e30e3063 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -230,9 +230,9 @@ describe('BackdropGen', () => { const tasks = Array.from(aigcMock.tasks.values()) .filter((record) => record.task.type === TaskType.GenerateBackdrop) .map((record) => record.task) - const task = tasks[tasks.length - 1] + const task = tasks[tasks.length - 1]! expect(adoptParams.taskIds).toHaveLength(1) - expect(adoptParams.taskIds[0]).toBe(task?.id) + expect(adoptParams.taskIds[0]).toBe(task.id) }) it('should exclude non-completed task IDs from recordAdoption', async () => { @@ -247,11 +247,9 @@ describe('BackdropGen', () => { // Manually modify the task status to simulate a failed task const taskRecords = Array.from(aigcMock.tasks.values()) .filter((record) => record.task.type === TaskType.GenerateBackdrop) - const taskRecord = taskRecords[taskRecords.length - 1] - if (taskRecord) { - taskRecord.task.status = TaskStatus.Failed - taskRecord.task.updatedAt = new Date().toISOString() - } + const taskRecord = taskRecords[taskRecords.length - 1]! + taskRecord.task.status = TaskStatus.Failed + taskRecord.task.updatedAt = new Date().toISOString() // Mock adoptAsset to inspect the taskIds parameter const adoptAssetCalls: unknown[] = [] diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index e63168540..5a874b934 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -196,10 +196,10 @@ describe('CostumeGen', () => { // Task should be completed, so getTaskIds should return it const tasks = Array.from(aigcMock.tasks.values()) - const task = tasks[tasks.length - 1]?.task + const task = tasks[tasks.length - 1]!.task const taskIds = gen.getTaskIds() expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(task?.id) + expect(taskIds[0]).toBe(task.id) }) it('should exclude non-completed task IDs from getTaskIds', async () => { @@ -213,11 +213,9 @@ describe('CostumeGen', () => { // Manually modify the task status to simulate a failed task const tasks = Array.from(aigcMock.tasks.values()) - const taskRecord = tasks[tasks.length - 1] - if (taskRecord) { - taskRecord.task.status = TaskStatus.Failed - taskRecord.task.updatedAt = new Date().toISOString() - } + const taskRecord = tasks[tasks.length - 1]! + taskRecord.task.status = TaskStatus.Failed + taskRecord.task.updatedAt = new Date().toISOString() // getTaskIds should return empty array since task is failed const taskIds = gen.getTaskIds() diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index 49c94c8e3..cc9dbff39 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -387,20 +387,14 @@ describe('SpriteGen', () => { // Manually modify task statuses to simulate failures // Mark the first sprite genImagesTask as failed (it's the first GenerateCostume task) - if (allCostumeTasks[0]) { - allCostumeTasks[0].task.status = TaskStatus.Failed - allCostumeTasks[0].task.updatedAt = new Date().toISOString() - } + allCostumeTasks[0]!.task.status = TaskStatus.Failed + allCostumeTasks[0]!.task.updatedAt = new Date().toISOString() // Mark the first costume's generateTask as cancelled (it's the second GenerateCostume task) - if (allCostumeTasks[1]) { - allCostumeTasks[1].task.status = TaskStatus.Cancelled - allCostumeTasks[1].task.updatedAt = new Date().toISOString() - } + allCostumeTasks[1]!.task.status = TaskStatus.Cancelled + allCostumeTasks[1]!.task.updatedAt = new Date().toISOString() // Mark the first animation's generateVideoTask as failed - if (allAnimationVideoTasks[0]) { - allAnimationVideoTasks[0].task.status = TaskStatus.Failed - allAnimationVideoTasks[0].task.updatedAt = new Date().toISOString() - } + allAnimationVideoTasks[0]!.task.status = TaskStatus.Failed + allAnimationVideoTasks[0]!.task.updatedAt = new Date().toISOString() // Mock adoptAsset to inspect the taskIds parameter const adoptAssetCalls: unknown[] = [] @@ -413,8 +407,8 @@ describe('SpriteGen', () => { // Verify that failed/cancelled tasks are excluded expect(adoptAssetCalls).toHaveLength(1) const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]?.task.id) - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]?.task.id) - expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]?.task.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]!.task.id) + expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]!.task.id) + expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]!.task.id) }) }) From c866ca48dbec9a1f1ad2a5b93b51787c3895af0f Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 01:57:30 +0000 Subject: [PATCH 09/12] chore(spx-gui): fix prettier formatting in test files Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.test.ts | 20 ++++++++++++-------- spx-gui/src/models/gen/backdrop-gen.test.ts | 5 +++-- spx-gui/src/models/gen/sprite-gen.test.ts | 10 ++++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 37f7140e1..8de92e769 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -343,10 +343,12 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify task statuses to simulate failures - const generateVideoTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) - const extractFramesTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.ExtractVideoFrames) + const generateVideoTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.GenerateAnimationVideo + ) + const extractFramesTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.ExtractVideoFrames + ) const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! generateVideoRecord.task.status = TaskStatus.Failed @@ -376,10 +378,12 @@ describe('AnimationGen', () => { await gen.extractFrames() // Manually modify one task to fail, keep the other completed - const generateVideoTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) - const extractFramesTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.ExtractVideoFrames) + const generateVideoTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.GenerateAnimationVideo + ) + const extractFramesTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.ExtractVideoFrames + ) const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! generateVideoRecord.task.status = TaskStatus.Failed diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index 5e30e3063..979c0a4bf 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -245,8 +245,9 @@ describe('BackdropGen', () => { await gen.finish() // Manually modify the task status to simulate a failed task - const taskRecords = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateBackdrop) + const taskRecords = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.GenerateBackdrop + ) const taskRecord = taskRecords[taskRecords.length - 1]! taskRecord.task.status = TaskStatus.Failed taskRecord.task.updatedAt = new Date().toISOString() diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index cc9dbff39..33fd878e5 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -380,10 +380,12 @@ describe('SpriteGen', () => { // Get tasks that we'll mark as failed // The order is: genImagesTask, then costume tasks, then animation tasks - const allCostumeTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateCostume) - const allAnimationVideoTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) + const allCostumeTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.GenerateCostume + ) + const allAnimationVideoTasks = Array.from(aigcMock.tasks.values()).filter( + (record) => record.task.type === TaskType.GenerateAnimationVideo + ) // Manually modify task statuses to simulate failures // Mark the first sprite genImagesTask as failed (it's the first GenerateCostume task) From af69ff7318085be22508606846c603fd7b213e1f Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 02:39:23 +0000 Subject: [PATCH 10/12] fix(spx-gui): use non-null assertions for cancel test scenarios Apply non-null assertions (!) to lastRecord in cancel test scenarios where we know the value exists after task cancellation. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.test.ts | 2 +- spx-gui/src/models/gen/backdrop-gen.test.ts | 2 +- spx-gui/src/models/gen/costume-gen.test.ts | 2 +- spx-gui/src/models/gen/sprite-gen.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 8de92e769..793435f48 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -294,7 +294,7 @@ describe('AnimationGen', () => { await expect(generatePromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) + expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) }) it('should only return completed task IDs in getTaskIds', async () => { diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index 979c0a4bf..5771675f1 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -204,7 +204,7 @@ describe('BackdropGen', () => { await expect(genImagesPromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) + expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) }) it('should only include completed task IDs in recordAdoption', async () => { diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index 5a874b934..06cb35499 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -182,7 +182,7 @@ describe('CostumeGen', () => { await expect(generatePromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) + expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) }) it('should only return completed task IDs in getTaskIds', async () => { diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index 33fd878e5..8c025b232 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -258,7 +258,7 @@ describe('SpriteGen', () => { await expect(genImagesPromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) + expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) }) it('should cancel running costume and animation generations', async () => { From 673e02ebc04a1c7f1ec5a8571e3db2d3eff6f21b Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 02:45:49 +0000 Subject: [PATCH 11/12] revert: remove all test case changes, keep only implementation changes Remove all changes to test files as requested, keeping only the core implementation changes that filter failed/incomplete tasks in asset adoption. Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.test.ts | 102 +------------------ spx-gui/src/models/gen/backdrop-gen.test.ts | 66 +----------- spx-gui/src/models/gen/costume-gen.test.ts | 39 +------ spx-gui/src/models/gen/sprite-gen.test.ts | 97 +----------------- 4 files changed, 8 insertions(+), 296 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.test.ts b/spx-gui/src/models/gen/animation-gen.test.ts index 793435f48..0c184aa07 100644 --- a/spx-gui/src/models/gen/animation-gen.test.ts +++ b/spx-gui/src/models/gen/animation-gen.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { AnimationLoopMode, ArtStyle, Perspective } from '@/apis/common' -import { TaskStatus, TaskType } from '@/apis/aigc' +import { TaskStatus } from '@/apis/aigc' import * as fileHelpers from '@/models/common/file' import { makeProject, mockFile } from '../common/test' import { setupAigcMock, MockAigcApis } from './aigc-mock' @@ -294,104 +294,6 @@ describe('AnimationGen', () => { await expect(generatePromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) - }) - - it('should only return completed task IDs in getTaskIds', async () => { - const project = makeProject() - const sprite = Sprite.create('TestSprite', '') - const defaultCostume = new Costume('default', mockFile()) - sprite.addCostume(defaultCostume) - project.addSprite(sprite) - const gen = new AnimationGen(sprite, project, { - description: 'A test animation', - referenceCostumeId: defaultCostume.id - }) - - await gen.enrich() - await gen.generateVideo() - gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) - await gen.extractFrames() - - // Both tasks should be completed - const generateVideoTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateAnimationVideo) - .map((record) => record.task) - const extractFramesTasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.ExtractVideoFrames) - .map((record) => record.task) - const taskIds = gen.getTaskIds() - expect(taskIds).toHaveLength(2) - expect(taskIds).toContain(generateVideoTasks[generateVideoTasks.length - 1]!.id) - expect(taskIds).toContain(extractFramesTasks[extractFramesTasks.length - 1]!.id) - }) - - it('should exclude non-completed task IDs from getTaskIds', async () => { - const project = makeProject() - const sprite = Sprite.create('TestSprite', '') - const defaultCostume = new Costume('default', mockFile()) - sprite.addCostume(defaultCostume) - project.addSprite(sprite) - const gen = new AnimationGen(sprite, project, { - description: 'A test animation', - referenceCostumeId: defaultCostume.id - }) - - await gen.enrich() - await gen.generateVideo() - gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) - await gen.extractFrames() - - // Manually modify task statuses to simulate failures - const generateVideoTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.GenerateAnimationVideo - ) - const extractFramesTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.ExtractVideoFrames - ) - const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! - const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! - generateVideoRecord.task.status = TaskStatus.Failed - generateVideoRecord.task.updatedAt = new Date().toISOString() - extractFramesRecord.task.status = TaskStatus.Cancelled - extractFramesRecord.task.updatedAt = new Date().toISOString() - - // getTaskIds should return empty array since both tasks are not completed - const taskIds = gen.getTaskIds() - expect(taskIds).toHaveLength(0) - }) - - it('should handle mixed task statuses in getTaskIds', async () => { - const project = makeProject() - const sprite = Sprite.create('TestSprite', '') - const defaultCostume = new Costume('default', mockFile()) - sprite.addCostume(defaultCostume) - project.addSprite(sprite) - const gen = new AnimationGen(sprite, project, { - description: 'A test animation', - referenceCostumeId: defaultCostume.id - }) - - await gen.enrich() - await gen.generateVideo() - gen.setFramesConfig({ startTime: 0, duration: 1000, interval: 200 }) - await gen.extractFrames() - - // Manually modify one task to fail, keep the other completed - const generateVideoTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.GenerateAnimationVideo - ) - const extractFramesTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.ExtractVideoFrames - ) - const generateVideoRecord = generateVideoTasks[generateVideoTasks.length - 1]! - const extractFramesRecord = extractFramesTasks[extractFramesTasks.length - 1]! - generateVideoRecord.task.status = TaskStatus.Failed - generateVideoRecord.task.updatedAt = new Date().toISOString() - - // getTaskIds should only return the completed extractFramesTask - const taskIds = gen.getTaskIds() - expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(extractFramesRecord.task.id) + expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) }) diff --git a/spx-gui/src/models/gen/backdrop-gen.test.ts b/spx-gui/src/models/gen/backdrop-gen.test.ts index 5771675f1..fec51baf0 100644 --- a/spx-gui/src/models/gen/backdrop-gen.test.ts +++ b/spx-gui/src/models/gen/backdrop-gen.test.ts @@ -1,7 +1,6 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach } from 'vitest' import { ArtStyle, BackdropCategory, Perspective } from '@/apis/common' -import { TaskStatus, TaskType } from '@/apis/aigc' -import * as aigcApis from '@/apis/aigc' +import { TaskStatus } from '@/apis/aigc' import { makeProject } from '../common/test' import { setupAigcMock, MockAigcApis } from './aigc-mock' import { BackdropGen } from './backdrop-gen' @@ -204,65 +203,6 @@ describe('BackdropGen', () => { await expect(genImagesPromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) - }) - - it('should only include completed task IDs in recordAdoption', async () => { - const project = makeProject() - const gen = new BackdropGen(project, 'A test backdrop') - - await gen.enrich() - await gen.genImages() - gen.setImage(gen.imagesGenState.result![0]) - await gen.finish() - - // Mock adoptAsset to inspect the taskIds parameter - const adoptAssetCalls: unknown[] = [] - vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { - adoptAssetCalls.push(params) - }) - - await gen.recordAdoption() - - // Verify that taskIds contains the task ID (task should be completed) - expect(adoptAssetCalls).toHaveLength(1) - const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - const tasks = Array.from(aigcMock.tasks.values()) - .filter((record) => record.task.type === TaskType.GenerateBackdrop) - .map((record) => record.task) - const task = tasks[tasks.length - 1]! - expect(adoptParams.taskIds).toHaveLength(1) - expect(adoptParams.taskIds[0]).toBe(task.id) - }) - - it('should exclude non-completed task IDs from recordAdoption', async () => { - const project = makeProject() - const gen = new BackdropGen(project, 'A test backdrop') - - await gen.enrich() - await gen.genImages() - gen.setImage(gen.imagesGenState.result![0]) - await gen.finish() - - // Manually modify the task status to simulate a failed task - const taskRecords = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.GenerateBackdrop - ) - const taskRecord = taskRecords[taskRecords.length - 1]! - taskRecord.task.status = TaskStatus.Failed - taskRecord.task.updatedAt = new Date().toISOString() - - // Mock adoptAsset to inspect the taskIds parameter - const adoptAssetCalls: unknown[] = [] - vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { - adoptAssetCalls.push(params) - }) - - await gen.recordAdoption() - - // Verify that taskIds is empty since the task is failed - expect(adoptAssetCalls).toHaveLength(1) - const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - expect(adoptParams.taskIds).toHaveLength(0) + expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) }) diff --git a/spx-gui/src/models/gen/costume-gen.test.ts b/spx-gui/src/models/gen/costume-gen.test.ts index 06cb35499..d5f3a76a8 100644 --- a/spx-gui/src/models/gen/costume-gen.test.ts +++ b/spx-gui/src/models/gen/costume-gen.test.ts @@ -182,43 +182,6 @@ describe('CostumeGen', () => { await expect(generatePromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) - }) - - it('should only return completed task IDs in getTaskIds', async () => { - const project = makeProject() - const sprite = Sprite.create('TestSprite', '') - project.addSprite(sprite) - const gen = new CostumeGen(sprite, project, { description: 'A test costume' }) - - await gen.enrich() - await gen.generate() - - // Task should be completed, so getTaskIds should return it - const tasks = Array.from(aigcMock.tasks.values()) - const task = tasks[tasks.length - 1]!.task - const taskIds = gen.getTaskIds() - expect(taskIds).toHaveLength(1) - expect(taskIds[0]).toBe(task.id) - }) - - it('should exclude non-completed task IDs from getTaskIds', async () => { - const project = makeProject() - const sprite = Sprite.create('TestSprite', '') - project.addSprite(sprite) - const gen = new CostumeGen(sprite, project, { description: 'A test costume' }) - - await gen.enrich() - await gen.generate() - - // Manually modify the task status to simulate a failed task - const tasks = Array.from(aigcMock.tasks.values()) - const taskRecord = tasks[tasks.length - 1]! - taskRecord.task.status = TaskStatus.Failed - taskRecord.task.updatedAt = new Date().toISOString() - - // getTaskIds should return empty array since task is failed - const taskIds = gen.getTaskIds() - expect(taskIds).toHaveLength(0) + expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) }) diff --git a/spx-gui/src/models/gen/sprite-gen.test.ts b/spx-gui/src/models/gen/sprite-gen.test.ts index 8c025b232..a7cc6bb7d 100644 --- a/spx-gui/src/models/gen/sprite-gen.test.ts +++ b/spx-gui/src/models/gen/sprite-gen.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { ArtStyle, Perspective, SpriteCategory } from '@/apis/common' -import { TaskStatus, TaskType } from '@/apis/aigc' -import * as aigcApis from '@/apis/aigc' +import { TaskStatus } from '@/apis/aigc' import { createI18n } from '@/utils/i18n' import * as fileHelpers from '@/models/common/file' import { makeProject } from '../common/test' @@ -258,7 +257,7 @@ describe('SpriteGen', () => { await expect(genImagesPromise).rejects.toThrow('cancelled') const lastRecord = Array.from(tasks.values()).at(-1) - expect(lastRecord!.task.status).toBe(TaskStatus.Cancelled) + expect(lastRecord?.task.status).toBe(TaskStatus.Cancelled) }) it('should cancel running costume and animation generations', async () => { @@ -321,96 +320,4 @@ describe('SpriteGen', () => { const sprite3 = gen3.finish() expect(sprite3.rotationStyle).toBe(RotationStyle.Normal) }) - - it('should only include completed task IDs in recordAdoption', async () => { - const project = makeProject() - const gen = new SpriteGen(createI18n({ lang: 'en' }), project, 'A test sprite') - - await gen.enrich() - await gen.genImages() - gen.setImage(gen.imagesGenState.result![0]) - await gen.prepareContent() - - // Finish all sub-generations - for (const costumeGen of gen.costumes) { - await finishCostumeGen(costumeGen.name, costumeGen) - } - for (const animationGen of gen.animations) { - await finishAnimationGen(animationGen.name, animationGen) - } - - const sprite = gen.finish() - project.addSprite(sprite) - - // Mock adoptAsset to inspect the taskIds parameter - const adoptAssetCalls: unknown[] = [] - vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { - adoptAssetCalls.push(params) - }) - - await gen.recordAdoption() - - // Verify that taskIds contains all completed task IDs - expect(adoptAssetCalls).toHaveLength(1) - const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - // Should include: genImagesTask + costume tasks + animation tasks (generateVideo + extractFrames) - const expectedCount = 1 + gen.costumes.length + gen.animations.length * 2 - expect(adoptParams.taskIds.length).toBeGreaterThanOrEqual(expectedCount) - }) - - it('should exclude failed/cancelled task IDs from recordAdoption', async () => { - const project = makeProject() - const gen = new SpriteGen(createI18n({ lang: 'en' }), project, 'A test sprite') - - await gen.enrich() - await gen.genImages() - gen.setImage(gen.imagesGenState.result![0]) - await gen.prepareContent() - - // Finish all sub-generations - for (const costumeGen of gen.costumes) { - await finishCostumeGen(costumeGen.name, costumeGen) - } - for (const animationGen of gen.animations) { - await finishAnimationGen(animationGen.name, animationGen) - } - - const sprite = gen.finish() - project.addSprite(sprite) - - // Get tasks that we'll mark as failed - // The order is: genImagesTask, then costume tasks, then animation tasks - const allCostumeTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.GenerateCostume - ) - const allAnimationVideoTasks = Array.from(aigcMock.tasks.values()).filter( - (record) => record.task.type === TaskType.GenerateAnimationVideo - ) - - // Manually modify task statuses to simulate failures - // Mark the first sprite genImagesTask as failed (it's the first GenerateCostume task) - allCostumeTasks[0]!.task.status = TaskStatus.Failed - allCostumeTasks[0]!.task.updatedAt = new Date().toISOString() - // Mark the first costume's generateTask as cancelled (it's the second GenerateCostume task) - allCostumeTasks[1]!.task.status = TaskStatus.Cancelled - allCostumeTasks[1]!.task.updatedAt = new Date().toISOString() - // Mark the first animation's generateVideoTask as failed - allAnimationVideoTasks[0]!.task.status = TaskStatus.Failed - allAnimationVideoTasks[0]!.task.updatedAt = new Date().toISOString() - - // Mock adoptAsset to inspect the taskIds parameter - const adoptAssetCalls: unknown[] = [] - vi.mocked(aigcApis.adoptAsset).mockImplementation(async (params) => { - adoptAssetCalls.push(params) - }) - - await gen.recordAdoption() - - // Verify that failed/cancelled tasks are excluded - expect(adoptAssetCalls).toHaveLength(1) - const adoptParams = adoptAssetCalls[0] as { taskIds: string[] } - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[0]!.task.id) - expect(adoptParams.taskIds).not.toContain(allCostumeTasks[1]!.task.id) - expect(adoptParams.taskIds).not.toContain(allAnimationVideoTasks[0]!.task.id) - }) }) From 6612f022625f7114c224b6ea0a07cbb0a777a6f2 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 9 Feb 2026 06:30:14 +0000 Subject: [PATCH 12/12] refactor(spx-gui): simplify getTaskIds docstring comments Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: nighca <1492263+nighca@users.noreply.github.com> --- spx-gui/src/models/gen/animation-gen.ts | 2 +- spx-gui/src/models/gen/costume-gen.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spx-gui/src/models/gen/animation-gen.ts b/spx-gui/src/models/gen/animation-gen.ts index c16f41bcb..028185806 100644 --- a/spx-gui/src/models/gen/animation-gen.ts +++ b/spx-gui/src/models/gen/animation-gen.ts @@ -76,7 +76,7 @@ export class AnimationGen extends Disposable { return reactive(this) as this } - /** Get task IDs for completed tasks only. Only completed task IDs will be returned. */ + /** Get IDs for (completed) tasks. */ getTaskIds() { return [this.generateVideoTask, this.extractFramesTask] .filter((t) => t.data?.status === TaskStatus.Completed) diff --git a/spx-gui/src/models/gen/costume-gen.ts b/spx-gui/src/models/gen/costume-gen.ts index e7a805504..a606664e9 100644 --- a/spx-gui/src/models/gen/costume-gen.ts +++ b/spx-gui/src/models/gen/costume-gen.ts @@ -61,7 +61,7 @@ export class CostumeGen extends Disposable { return reactive(this) as this } - /** Get task IDs for completed tasks only. Only completed task IDs will be returned. */ + /** Get IDs for (completed) tasks. */ getTaskIds() { if (this.generateTask.data?.status !== TaskStatus.Completed) return [] return [this.generateTask.data.id]