From 5f510e4176b15f829625ebf144769f292917c1de Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Fri, 25 Sep 2026 19:19:23 +0800 Subject: [PATCH 1/8] feat: add Resolve R1 deterministic artifact tracer --- .agents/skills/flovart/SKILL.md | 50 +++++ .claude/skills/flovart/SKILL.md | 50 +++++ docs/content/docs/progress/pending-test.mdx | 18 +- skills/flovart/SKILL.md | 50 +++++ src-tauri/src/runtime/mod.rs | 155 ++++++++++++-- src-tauri/src/runtime/store.rs | 21 ++ src-tauri/src/runtime/worker.rs | 65 ++++++ src-tauri/tests/resolve_r1_artifact.rs | 202 ++++++++++++++++++ tests/flovartRuntimeRegistry.test.ts | 9 +- .../runtime/command-registry.v1.json | 16 +- tools/flovart/runtime-command-surface.js | 3 + 11 files changed, 618 insertions(+), 21 deletions(-) create mode 100644 src-tauri/tests/resolve_r1_artifact.rs diff --git a/.agents/skills/flovart/SKILL.md b/.agents/skills/flovart/SKILL.md index 1cfd0896..5f7f7b31 100644 --- a/.agents/skills/flovart/SKILL.md +++ b/.agents/skills/flovart/SKILL.md @@ -64,6 +64,56 @@ code, inspect before retrying, and stop safely when the visible workspace is unavailable or the target/revision changed. Never choose another project or retry a mutation with a new identity. +## Resolve Studio 21.1 — no-provider Media Pool tracer + +Use this path only for the Resolve R0/R1 host-validation task. Resolve owns +project, timeline, selection, and Media Pool state. Flovart CLI owns the local +deterministic fixture task and its durable Artifact. + +Before starting, verify the installed product is Resolve Studio 21.1 and that +its native MCP is connected to the same local Resolve instance. Discover the +tool names and current API through that MCP; do not guess from another Resolve +version. If 21.1 or the native MCP is unavailable, stop and report the exact +host gate instead of presenting a mock as Resolve evidence. + +For the no-provider tracer: + +1. Use Resolve native MCP to read the current project, timeline, and selected + clip. Keep their stable identities and the timeline state for this task; + recheck them before a Resolve write. If the selected target changes, stop + instead of retargeting. +2. Submit the fixed local fixture with one unique idempotency key: + + ```bash + flovart runtime.test.fixture-image --idempotency-key --json + ``` + + From this source checkout, use `npm run flovart:cli --` before the command. + If submission status is unknown, retry with the same key, never a new one. +3. Poll the returned task ID with `flovart task.get --task-id --json` + until it is completed. Stop on failure or cancellation. +4. Resolve the completed file immediately before import: + + ```bash + flovart artifact.locate --task-id --json + ``` + + The result is a local path plus the verified MIME type, byte size, and + SHA-256. Use that path only for the immediate host import; do not write it + into project metadata. +5. Use a bounded Media Pool import operation exposed by the installed Resolve + native MCP. Do not add the fixture to the Timeline, replace a clip, delete + or overwrite media, or run unrestricted scripts. If the installed MCP has + no safe bounded import path, stop and report that concrete gap. +6. Re-read the Media Pool and Timeline through Resolve MCP. Confirm the item + appears in the Media Pool and the Timeline state is unchanged. + +`runtime.test.fixture-image` imports the repository's fixed `public/favicon.png` +as a no-provider fixture. It verifies the local task-to-file handoff only; it +does not use the selected clip as a visual reference and is not AI generation. +Never describe it as a generated scene or as a completed real-host tracer until +Resolve visibly accepts the file and the Timeline comparison passes. + ## Cold start — no project open `workflow.inspect` returns `WORKSPACE_UNAVAILABLE` when the browser is diff --git a/.claude/skills/flovart/SKILL.md b/.claude/skills/flovart/SKILL.md index 1cfd0896..5f7f7b31 100644 --- a/.claude/skills/flovart/SKILL.md +++ b/.claude/skills/flovart/SKILL.md @@ -64,6 +64,56 @@ code, inspect before retrying, and stop safely when the visible workspace is unavailable or the target/revision changed. Never choose another project or retry a mutation with a new identity. +## Resolve Studio 21.1 — no-provider Media Pool tracer + +Use this path only for the Resolve R0/R1 host-validation task. Resolve owns +project, timeline, selection, and Media Pool state. Flovart CLI owns the local +deterministic fixture task and its durable Artifact. + +Before starting, verify the installed product is Resolve Studio 21.1 and that +its native MCP is connected to the same local Resolve instance. Discover the +tool names and current API through that MCP; do not guess from another Resolve +version. If 21.1 or the native MCP is unavailable, stop and report the exact +host gate instead of presenting a mock as Resolve evidence. + +For the no-provider tracer: + +1. Use Resolve native MCP to read the current project, timeline, and selected + clip. Keep their stable identities and the timeline state for this task; + recheck them before a Resolve write. If the selected target changes, stop + instead of retargeting. +2. Submit the fixed local fixture with one unique idempotency key: + + ```bash + flovart runtime.test.fixture-image --idempotency-key --json + ``` + + From this source checkout, use `npm run flovart:cli --` before the command. + If submission status is unknown, retry with the same key, never a new one. +3. Poll the returned task ID with `flovart task.get --task-id --json` + until it is completed. Stop on failure or cancellation. +4. Resolve the completed file immediately before import: + + ```bash + flovart artifact.locate --task-id --json + ``` + + The result is a local path plus the verified MIME type, byte size, and + SHA-256. Use that path only for the immediate host import; do not write it + into project metadata. +5. Use a bounded Media Pool import operation exposed by the installed Resolve + native MCP. Do not add the fixture to the Timeline, replace a clip, delete + or overwrite media, or run unrestricted scripts. If the installed MCP has + no safe bounded import path, stop and report that concrete gap. +6. Re-read the Media Pool and Timeline through Resolve MCP. Confirm the item + appears in the Media Pool and the Timeline state is unchanged. + +`runtime.test.fixture-image` imports the repository's fixed `public/favicon.png` +as a no-provider fixture. It verifies the local task-to-file handoff only; it +does not use the selected clip as a visual reference and is not AI generation. +Never describe it as a generated scene or as a completed real-host tracer until +Resolve visibly accepts the file and the Timeline comparison passes. + ## Cold start — no project open `workflow.inspect` returns `WORKSPACE_UNAVAILABLE` when the browser is diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 67a647f3..21e51ccf 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -4,11 +4,19 @@ title: 待测试确认 本页记录实际进入文档或代码、仍需用户确认的变更。既有实现的构建、全量测试、Rust Runtime 和响应式浏览器记录见[发行候选证据](../../../evidence/release-candidate/README.md),仅适用于其对应代码与范围;本页每一节分别注明对应范围的验证结果。真实第三方宿主、账号、Provider 与公开发布仍需单独认证。 -> **2026-09-27 当前状态**:第一宿主仍是 Resolve Studio 21.1。当前工作树新增了 Resolve 面板、候选导入状态与 durable artifact 的源码实现;它们仍未在 Resolve Studio 21.1 真机运行。宿主 native MCP 由 Resolve 提供,Iris 不应再造一套 MCP Server。AE 源码/候选完整性工作保留为 Experimental evidence,不是当前第一交付 gate。 +> **2026-09-27 当前状态**:第一宿主仍是 Resolve Studio 21.1。main 已合入 Resolve 面板、候选导入状态与 durable artifact 的源码实现;它们仍未在 Resolve Studio 21.1 真机运行。宿主 native MCP 由 Resolve 提供,Iris 不应再造一套 MCP Server。AE 源码/候选完整性工作保留为 Experimental evidence,不是当前第一交付 gate。 +## Resolve R1:deterministic artifact Runtime slice -## 当前 worktree:Resolve 面板与 durable candidate 源码 +- 已实现:`runtime.test.fixture-image` 通过既有 Runtime task ledger 和 Worker,把仓库内固定 `public/favicon.png` 作为 PNG fixture 持久化到 Runtime Artifact 目录;提交要求幂等键,不调用 Provider,任务结果明确记录 `providerCalled: false` 与 `costMicros: 0`。 +- 已实现:`artifact.locate --task-id ` 只为 completed task 返回本机持久文件路径;复用 store-relative path containment 校验,并重核文件类型、字节数和 SHA-256。Runtime `read_artifact` 也执行同样的完整性检查;同尺寸内容篡改会失败。 +- 本地验证:`cargo test --manifest-path src-tauri/Cargo.toml --test resolve_r1_artifact -- --nocapture` 通过 3/3,覆盖幂等提交、Runtime 重开后的 Artifact 定位/读取、未完成任务拒绝和同尺寸篡改拒绝;运行时设置 `TAURI_CONFIG={"bundle":{"externalBin":[]}}` 是因为隔离 worktree 没有 Agent sidecar,此次测试没有验证安装包 sidecar。`npx vitest run tests/flovartRuntimeTasks.test.ts tests/flovartRuntimeRegistry.test.ts tests/studioResolveInspector.test.ts` 通过 18/18;`npx tsc --noEmit` 在安装 `dsh-plugin` 锁定依赖后通过;`npm run docs:check` 与 `npm run studio:build` 通过。Studio build 日志仍报告未配置 AE SDK / Resolve WorkflowIntegration.node,这不是 Resolve 宿主验证。 +- CLI tracer 顺序:`runtime.test.fixture-image --idempotency-key ` → `task.get --task-id ` → `artifact.locate --task-id `。最后一步只把通过校验的持久本机路径交给同机 Resolve import;不得写进项目元数据或外部服务。 +- Flovart canonical Skill 及 `.agents` / `.claude` 镜像记录了 Resolve 21.1 R0/R1 tracer 的目标复核、幂等提交、bounded Media Pool import 与 Timeline 零变化要求;fixture 明确不是根据选中素材生成的 AI 结果。 +- 仍待真实宿主验证:Resolve Studio 21.1 native MCP 连接、选中媒体上下文冻结、该路径能否被宿主安全导入 Media Pool、Timeline 保持不变以及重启后素材可用。以上本地 Runtime/CLI 测试不代表 Resolve 导入成功,也没有把 R1 Golden Task 标记完成。 -状态:**源码已改,真实宿主尚未验证;当前改动仍在未提交 worktree。** 相关实现位于 `integrations/studio/resolve/` 与 `services/studio/`。 +## 当前 main:Resolve 面板与 durable candidate 源码 + +状态:**源码已合入 main,真实宿主尚未验证。** 相关实现位于 `integrations/studio/resolve/` 与 `services/studio/`;本节记录源码状态,不代表完整产品路径可达。 - 已实现源码:Resolve Inspector 路径加入中英文界面、Current Clip / Generate / References / Model / Output / Task / Candidates 与低权重 Open in Iris footer;Resolve 的 `Output` 提供 `Media Pool`,旧 `制作` / `本次记录` tab 不再用于该路径。候选提供 Preview、Add to Media Pool、确认/拒绝/未知状态;unknown 状态提示先检查 Media Pool,并禁用盲重试。 - 已实现但未接入产品路径的源码:`StudioWorkflowController.prepareCandidate()` 可复用现有生成路径,校验 Blob、声明字节数和 SHA-256,持久化候选并保留冻结 target;`importCandidate()` 对同一候选使用冻结 target,成功可幂等返回,结果未知时锁存并禁止自动重发。当前 `installStudioBrowserLink()` 没有生产调用者,而 `resolve/index.js` 只读取全局 controller;因此该 controller 与 Resolve 面板之间尚未完成生产 wiring,不能声称这些动作已从真实面板可达。 @@ -17,9 +25,9 @@ title: 待测试确认 - 当前本机发现:Windows 11 Pro for Workstations `10.0.26200`(build `26200`);Resolve 注册表版本为 `21.0.00020`,并非目标 21.1,且注册表信息本身不能确认 Studio license/edition;Resolve 进程检查时未运行;Node `v24.14.0` / npm `11.9.0`;在 Resolve 安装路径与默认 ProgramData SamplePlugin 路径均未发现 `WorkflowIntegration.node`。未捕获 native MCP / Codex 连接与 Resolve selection 证据。详见 [Resolve 真机验收清单](../../../../integrations/studio/RESOLVE_REAL_HOST_CHECKLIST.md)。 - Chromium Resolve-panel fixture matrix:`.tmp/resolve-panel-browser-matrix.mjs` 以 mock host/controller 挂载共享 Inspector;在 280、300、320、360、400、480、560、768、1024、1920px 检查均无横向溢出,`Output` 选项存在且默认值为 `Media Pool`,候选导入成功态可在 English / 简体中文之间切换。矩阵与截图在 `.tmp/resolve-panel-browser-evidence/`(含 `matrix.json`;记录 `pageErrors: []`)。这是 Chromium fixture,不是真实 Resolve/Studio/MCP 证据,也没有调用 Resolve API。 - Chrome app smoke:`npm run test:browser:chrome` 使用隔离动态端口并等待应用 mounted;本轮 mounted 成功,未观察到 console/page errors。等待 harness 的默认 timeout 为 45 秒。该 smoke 验证 Iris 浏览器应用可挂载,不验证 Resolve 面板在宿主内加载或 Media Pool 导入。 -- 编排缺口(源码审计):稳定 `workflow.node.run` 只返回不透明 artifact identity;Browser artifact registry 是 30 分钟 / 最多 64 项的内存索引,Blob 读取仅在应用内 `artifactGet` seam,CLI 没有 Browser artifact 物化命令。现有 Agent/Browser 桥使用 JSON/SSE,Agent 请求体整体缓冲上限 36 MiB,base64 可用素材约 27 MiB;没有二进制流、分块确认或断点恢复,因此不能把它当作 64 MiB 素材通道。Resolve `persistArtifact` 的 IPC 落盘模块也尚未接生产 controller。Runtime 按 task ID 提供的字节端点没有对应 CLI 导出合同,且不对应 Browser Workflow artifact identity;不得把内部 `storeRelpath` 当公开路径。必须先定义并验证窄的同机 binary handoff,再声称 Agent CLI 能把 artifact 交给 Resolve。 +- 编排缺口(源码审计):稳定 `workflow.node.run` 只返回不透明 artifact identity;Browser artifact registry 是 30 分钟 / 最多 64 项的内存索引,Blob 读取仅在应用内 `artifactGet` seam,CLI 没有 Browser artifact 物化命令。现有 Agent/Browser 桥使用 JSON/SSE,Agent 请求体整体缓冲上限 36 MiB,base64 可用素材约 27 MiB;没有二进制流、分块确认或断点恢复,因此不能把它当作 64 MiB 素材通道。Resolve `persistArtifact` 的 IPC 落盘模块也尚未接生产 controller。Resolve R1 现在增加 `artifact.locate --task-id` CLI 合同:只为 completed Runtime task 返回经字节数与 SHA-256 复核的持久本机路径,不公开内部 `storeRelpath`。该路径合同尚未通过真实 Resolve native MCP 验证;它也不映射 Browser Workflow artifact identity。Browser Workflow 的大素材仍无 binary handoff,不能把 36 MiB JSON/SSE 路径当作素材通道。 - 本地门禁:`npx tsc --noEmit` 通过;锁文件依赖下 `npm test -- --exclude '**/.tmp/**'` 为 `178 files passed`、`1196 passed / 1 skipped`;`npm run test:critical` 为 `10/10`,每轮 9 files / 53 tests;定向面板与 Skill 回归 8 files / 81 tests;`npm run build`、`npm run ext:build`、`npm run docs:check`、`npm run release:red-team`、`npm run release:secret-audit` 通过;`eval:validate` 检查 65 tasks / 57 predicates 通过,`eval:redteam` 为 12/12。`npm run studio:build` 能生成 4 个包,但日志显示 AE SDK 与 Resolve `WorkflowIntegration.node` 均缺失,输出不是已验证可运行的宿主包。本地结果不是 GitHub Hosted CI 结果,也不验证 Resolve 宿主。 -- Hosted CI 历史基线:run `36252046485`(HEAD `539a8e8`)记录为 7 个测试文件、16 项失败、1147 passed;本 worktree 的本地结果发生在当前远端 main 之前。当前本地 `origin/main` ref 为 `721e96e2`,与用户提供的远端 HEAD 一致;此前 `git fetch origin main` 曾遇到 GitHub TLS `SSL_ERROR_SYSCALL`,此分支 HEAD 仍为 `539a8e8`。Hosted CI 修复后结果尚无;`modelFetcher.ts` 曾有的 3 个 TypeScript 错误已由 PR #19 修复,不是当前 blocker。 +- Hosted CI 历史基线:run `36252046485`(HEAD `539a8e8`)与 `721e96e2` 均为旧基线,只用于解释此前失败来源,不代表当前 main 状态;`modelFetcher.ts` 曾有的 3 个 TypeScript 错误已由 PR #19 修复。当前状态以最新 main commit 的 GitHub Actions 为准。 ## 历史基线:UI foundation 首轮(2026-09-25 记录) diff --git a/skills/flovart/SKILL.md b/skills/flovart/SKILL.md index 1cfd0896..5f7f7b31 100644 --- a/skills/flovart/SKILL.md +++ b/skills/flovart/SKILL.md @@ -64,6 +64,56 @@ code, inspect before retrying, and stop safely when the visible workspace is unavailable or the target/revision changed. Never choose another project or retry a mutation with a new identity. +## Resolve Studio 21.1 — no-provider Media Pool tracer + +Use this path only for the Resolve R0/R1 host-validation task. Resolve owns +project, timeline, selection, and Media Pool state. Flovart CLI owns the local +deterministic fixture task and its durable Artifact. + +Before starting, verify the installed product is Resolve Studio 21.1 and that +its native MCP is connected to the same local Resolve instance. Discover the +tool names and current API through that MCP; do not guess from another Resolve +version. If 21.1 or the native MCP is unavailable, stop and report the exact +host gate instead of presenting a mock as Resolve evidence. + +For the no-provider tracer: + +1. Use Resolve native MCP to read the current project, timeline, and selected + clip. Keep their stable identities and the timeline state for this task; + recheck them before a Resolve write. If the selected target changes, stop + instead of retargeting. +2. Submit the fixed local fixture with one unique idempotency key: + + ```bash + flovart runtime.test.fixture-image --idempotency-key --json + ``` + + From this source checkout, use `npm run flovart:cli --` before the command. + If submission status is unknown, retry with the same key, never a new one. +3. Poll the returned task ID with `flovart task.get --task-id --json` + until it is completed. Stop on failure or cancellation. +4. Resolve the completed file immediately before import: + + ```bash + flovart artifact.locate --task-id --json + ``` + + The result is a local path plus the verified MIME type, byte size, and + SHA-256. Use that path only for the immediate host import; do not write it + into project metadata. +5. Use a bounded Media Pool import operation exposed by the installed Resolve + native MCP. Do not add the fixture to the Timeline, replace a clip, delete + or overwrite media, or run unrestricted scripts. If the installed MCP has + no safe bounded import path, stop and report that concrete gap. +6. Re-read the Media Pool and Timeline through Resolve MCP. Confirm the item + appears in the Media Pool and the Timeline state is unchanged. + +`runtime.test.fixture-image` imports the repository's fixed `public/favicon.png` +as a no-provider fixture. It verifies the local task-to-file handoff only; it +does not use the selected clip as a visual reference and is not AI generation. +Never describe it as a generated scene or as a completed real-host tracer until +Resolve visibly accepts the file and the Timeline comparison passes. + ## Cold start — no project open `workflow.inspect` returns `WORKSPACE_UNAVAILABLE` when the browser is diff --git a/src-tauri/src/runtime/mod.rs b/src-tauri/src/runtime/mod.rs index 650fcbed..85800339 100644 --- a/src-tauri/src/runtime/mod.rs +++ b/src-tauri/src/runtime/mod.rs @@ -33,6 +33,7 @@ use serde::Serialize; use serde_json::Value; use sha2::{Digest, Sha256}; use std::{ + io::Read, path::{Path, PathBuf}, sync::Arc, }; @@ -176,7 +177,71 @@ impl ProductionRuntime { } pub fn read_artifact(&self, task_id: &str) -> Result { + let (path, mime_type, expected_sha256, expected_byte_size) = self.artifact_file(task_id)?; + let bytes = std::fs::read(path).map_err(|_| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact cannot be read") + })?; + let actual_sha256 = hex::encode(Sha256::digest(&bytes)); + if bytes.len() as u64 != expected_byte_size || actual_sha256 != expected_sha256 { + return Err(RuntimeError::new( + "RUNTIME_UNAVAILABLE", + "Media artifact failed integrity verification", + )); + } + Ok(RuntimeArtifactPayload { mime_type, bytes }) + } + + pub fn locate_artifact(&self, task_id: &str) -> Result { + let (path, mime_type, expected_sha256, expected_byte_size) = self.artifact_file(task_id)?; + let mut file = std::fs::File::open(&path).map_err(|_| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact cannot be read") + })?; + let actual_byte_size = file + .metadata() + .map_err(|_| RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact cannot be read"))? + .len(); + if actual_byte_size != expected_byte_size { + return Err(RuntimeError::new( + "RUNTIME_UNAVAILABLE", + "Media artifact failed integrity verification", + )); + } + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let bytes_read = file.read(&mut buffer).map_err(|_| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact cannot be read") + })?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let actual_sha256 = hex::encode(hasher.finalize()); + if actual_sha256 != expected_sha256 { + return Err(RuntimeError::new( + "RUNTIME_UNAVAILABLE", + "Media artifact failed integrity verification", + )); + } + Ok(serde_json::json!({ + "taskId": task_id, + "artifactId": format!("sha256:{expected_sha256}"), + "path": path.to_string_lossy(), + "mimeType": mime_type, + "sha256": expected_sha256, + "byteSize": expected_byte_size + })) + } + + fn artifact_file(&self, task_id: &str) -> Result<(PathBuf, String, String, u64), RuntimeError> { let task = self.get_task(task_id)?; + if task.status != "completed" { + return Err(RuntimeError::new( + "TASK_NOT_COMPLETED", + "Media artifact is available only after its task completes", + )); + } let artifact = task .result .as_ref() @@ -197,20 +262,48 @@ impl ProductionRuntime { let path = validate_store_relpath(store_relpath, root).map_err(|message| { RuntimeError::new("RUNTIME_UNAVAILABLE", message) })?; - let bytes = std::fs::read(path).map_err(|error| { - RuntimeError::new( - "RUNTIME_UNAVAILABLE", - format!("Media artifact cannot be read: {error}"), - ) + let metadata = std::fs::metadata(&path).map_err(|_| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact cannot be read") })?; - Ok(RuntimeArtifactPayload { - mime_type: artifact - .get("mimeType") - .and_then(Value::as_str) - .unwrap_or("application/octet-stream") - .to_owned(), - bytes, - }) + let expected_sha256 = artifact + .get("sha256") + .and_then(Value::as_str) + .ok_or_else(|| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact checksum is missing") + })?; + if expected_sha256.len() != 64 + || !expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return Err(RuntimeError::new( + "RUNTIME_UNAVAILABLE", + "Media artifact checksum is invalid", + )); + } + let expected_byte_size = artifact + .get("byteSize") + .and_then(Value::as_u64) + .ok_or_else(|| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact size is missing") + })?; + if !metadata.is_file() || metadata.len() != expected_byte_size { + return Err(RuntimeError::new( + "RUNTIME_UNAVAILABLE", + "Media artifact failed integrity verification", + )); + } + let mime_type = artifact + .get("mimeType") + .and_then(Value::as_str) + .ok_or_else(|| { + RuntimeError::new("RUNTIME_UNAVAILABLE", "Media artifact MIME type is missing") + })? + .to_owned(); + Ok(( + path, + mime_type, + expected_sha256.to_ascii_lowercase(), + expected_byte_size, + )) } pub fn list_tasks( @@ -480,6 +573,34 @@ impl ProductionRuntime { ) .and_then(to_value) } + "runtime.test.fixture-image" => { + let idempotency_key = envelope + .get("idempotencyKey") + .and_then(Value::as_str) + .ok_or_else(|| { + RuntimeError::new( + "INVALID_ARGUMENT", + "runtime.test.fixture-image requires idempotencyKey", + ) + })?; + let args = &envelope["args"]; + validate_exact_args(args, &[])?; + let payload_hash = Self::hash_payload(&serde_json::json!({ + "command": "runtime.test.fixture-image", + "args": args, + })) + .map_err(|error| RuntimeError::new("RUNTIME_UNAVAILABLE", error.to_string()))?; + self.store + .submit_fixture_image( + envelope["commandId"].as_str().unwrap_or_default(), + envelope["actor"]["kind"].as_str().unwrap_or_default(), + envelope["actor"]["instanceId"].as_str().unwrap_or_default(), + idempotency_key, + &payload_hash, + args, + ) + .and_then(to_value) + } "production.dry-run" => { let idempotency_key = envelope .get("idempotencyKey") @@ -917,6 +1038,14 @@ impl ProductionRuntime { })?; self.get_task(task_id).and_then(to_value) } + "artifact.locate" => { + let args = &envelope["args"]; + validate_exact_args(args, &["taskId"])?; + let task_id = args.get("taskId").and_then(Value::as_str).ok_or_else(|| { + RuntimeError::new("INVALID_ARGUMENT", "artifact.locate requires taskId") + })?; + self.locate_artifact(task_id) + } "task.inspect" => { let args = &envelope["args"]; validate_exact_args(args, &["taskId"])?; diff --git a/src-tauri/src/runtime/store.rs b/src-tauri/src/runtime/store.rs index 2aaa2a33..bab4efc2 100644 --- a/src-tauri/src/runtime/store.rs +++ b/src-tauri/src/runtime/store.rs @@ -121,6 +121,27 @@ impl RuntimeStore { ) } + pub fn submit_fixture_image( + &self, + command_id: &str, + actor_kind: &str, + actor_instance_id: &str, + idempotency_key: &str, + payload_hash: &str, + args: &Value, + ) -> Result { + self.submit_task( + command_id, + actor_kind, + actor_instance_id, + idempotency_key, + payload_hash, + "runtime.test.fixture-image", + args, + 100, + ) + } + #[allow(clippy::too_many_arguments)] pub fn submit_image( &self, diff --git a/src-tauri/src/runtime/worker.rs b/src-tauri/src/runtime/worker.rs index 4ffdb431..1f0f29ec 100644 --- a/src-tauri/src/runtime/worker.rs +++ b/src-tauri/src/runtime/worker.rs @@ -32,6 +32,7 @@ const SAFE_POINT: Duration = Duration::from_millis(10); const VIDEO_POLL: Duration = Duration::from_secs(10); const SCHEDULER_POLL: Duration = Duration::from_secs(2); const LEASE_MS: i64 = 500; +const RESOLVE_R1_FIXTURE_IMAGE: &[u8] = include_bytes!("../../../public/favicon.png"); pub struct RuntimeWorker { stopping: Arc, @@ -107,6 +108,13 @@ impl RuntimeWorker { "runtime.test.delay" => { run_delay(&store, &executor_id, &executor_stopping, &task) } + "runtime.test.fixture-image" => run_fixture_image( + &store, + &executor_id, + &executor_stopping, + executor_artifact_root.as_deref(), + &task, + ), "production.dry-run" => run_production_plan(&store, &executor_id, &task), "audio.tts" => local_media::run_tts( &store, @@ -784,6 +792,63 @@ fn run_delay(store: &RuntimeStore, worker_id: &str, stopping: &AtomicBool, task: } } +fn run_fixture_image( + store: &RuntimeStore, + worker_id: &str, + stopping: &AtomicBool, + artifact_root: Option<&Path>, + task: &ClaimedTask, +) { + if stopping.load(Ordering::Acquire) || cancellation_requested(store, task, worker_id) { + return; + } + let Some(artifact_root) = artifact_root else { + fail( + store, + task, + worker_id, + "ARTIFACT_STORAGE_UNAVAILABLE", + "Runtime artifact storage is unavailable.", + ); + return; + }; + match persist_media( + artifact_root, + &task.id, + "image", + "png", + RESOLVE_R1_FIXTURE_IMAGE, + ) { + Ok((store_relpath, sha256, byte_size)) => { + let artifact_id = format!("sha256:{sha256}"); + let _ = store.complete_task( + &task.id, + worker_id, + &json!({ + "fixture": "resolve-r1-image-v1", + "providerCalled": false, + "costMicros": 0, + "artifact": { + "artifactId": artifact_id, + "kind": "image", + "mimeType": "image/png", + "storeRelpath": store_relpath, + "sha256": sha256, + "byteSize": byte_size + } + }), + ); + } + Err(_) => fail( + store, + task, + worker_id, + "ARTIFACT_WRITE_FAILED", + "Runtime could not persist the deterministic image fixture.", + ), + } +} + fn run_video( store: &RuntimeStore, worker_id: &str, diff --git a/src-tauri/tests/resolve_r1_artifact.rs b/src-tauri/tests/resolve_r1_artifact.rs new file mode 100644 index 00000000..96648df5 --- /dev/null +++ b/src-tauri/tests/resolve_r1_artifact.rs @@ -0,0 +1,202 @@ +use flovart_lib::runtime::ProductionRuntime; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::{ + fs, + path::{Path, PathBuf}, + thread, + time::{Duration, Instant}, +}; +use uuid::Uuid; + +struct TestWorkspace(PathBuf); + +impl TestWorkspace { + fn create() -> Self { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join(".tmp") + .join(format!("resolve-r1-{}", Uuid::now_v7())); + fs::create_dir_all(&path).expect("create isolated Runtime workspace"); + Self(path) + } + + fn database_path(&self) -> PathBuf { + self.0.join("state.db") + } + + fn path(&self) -> &Path { + &self.0 + } +} + +impl Drop for TestWorkspace { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn envelope(command: &str, args: Value, idempotency_key: Option<&str>) -> Value { + let mut envelope = json!({ + "protocolVersion": "1", + "commandId": ProductionRuntime::new_id("cmd"), + "command": command, + "args": args, + "actor": { "kind": "cli", "instanceId": "cli_resolve_r1_test" } + }); + if let Some(idempotency_key) = idempotency_key { + envelope["idempotencyKey"] = json!(idempotency_key); + } + envelope +} + +fn await_completed_task(runtime: &ProductionRuntime, task_id: &str) -> Value { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let task = runtime + .execute(&envelope("task.get", json!({ "taskId": task_id }), None)) + .expect("read fixture task"); + if task["status"] == "completed" { + return task; + } + assert_ne!(task["status"], "failed", "fixture task failed: {task}"); + assert_ne!( + task["status"], "cancelled", + "fixture task was cancelled: {task}" + ); + assert!( + Instant::now() < deadline, + "fixture task did not complete: {task}" + ); + thread::sleep(Duration::from_millis(20)); + } +} + +#[test] +fn resolve_fixture_task_returns_a_durable_verified_png_path() { + let workspace = TestWorkspace::create(); + let runtime = ProductionRuntime::open(env!("CARGO_PKG_VERSION"), &workspace.database_path()) + .expect("open Runtime"); + let first = runtime + .execute(&envelope( + "runtime.test.fixture-image", + json!({}), + Some("resolve-fixture-once"), + )) + .expect("submit deterministic image fixture"); + let replay = runtime + .execute(&envelope( + "runtime.test.fixture-image", + json!({}), + Some("resolve-fixture-once"), + )) + .expect("replay deterministic image fixture"); + + assert_eq!(replay, first, "idempotent retry must reuse the same task"); + assert_eq!(first["kind"], "task"); + let task_id = first["taskId"].as_str().expect("task id"); + let task = await_completed_task(&runtime, task_id); + let artifact = &task["result"]["artifact"]; + assert_eq!(artifact["kind"], "image"); + assert_eq!(artifact["mimeType"], "image/png"); + assert_eq!(task["result"]["providerCalled"], false); + assert_eq!(task["result"]["costMicros"], 0); + + drop(runtime); + let reopened = ProductionRuntime::open(env!("CARGO_PKG_VERSION"), &workspace.database_path()) + .expect("reopen Runtime with the same artifact store"); + let located = reopened + .execute(&envelope( + "artifact.locate", + json!({ "taskId": task_id }), + None, + )) + .expect("locate the persisted artifact"); + let path = PathBuf::from(located["path"].as_str().expect("artifact path")); + let root = workspace + .path() + .canonicalize() + .expect("canonical test root"); + let canonical_path = path.canonicalize().expect("canonical artifact file"); + assert!(canonical_path.starts_with(&root)); + + let bytes = fs::read(&canonical_path).expect("read persisted fixture"); + let payload = reopened + .read_artifact(task_id) + .expect("read artifact after Runtime reopen"); + let expected_sha256 = hex::encode(Sha256::digest(&bytes)); + assert_eq!(payload.mime_type, "image/png"); + assert_eq!(payload.bytes, bytes); + assert!(bytes.starts_with(b"\x89PNG\r\n\x1a\n")); + assert_eq!(located["taskId"], task_id); + assert_eq!(located["mimeType"], "image/png"); + assert_eq!(located["byteSize"].as_u64(), Some(bytes.len() as u64)); + assert_eq!(located["sha256"], expected_sha256); + assert_eq!(located["artifactId"], format!("sha256:{expected_sha256}")); +} + +#[test] +fn same_size_artifact_tampering_is_rejected_by_locate_and_read() { + let workspace = TestWorkspace::create(); + let runtime = ProductionRuntime::open(env!("CARGO_PKG_VERSION"), &workspace.database_path()) + .expect("open Runtime"); + let receipt = runtime + .execute(&envelope( + "runtime.test.fixture-image", + json!({}), + Some("resolve-fixture-tamper"), + )) + .expect("submit deterministic image fixture"); + let task_id = receipt["taskId"].as_str().expect("task id"); + await_completed_task(&runtime, task_id); + let located = runtime + .execute(&envelope( + "artifact.locate", + json!({ "taskId": task_id }), + None, + )) + .expect("locate the persisted artifact"); + let path = PathBuf::from(located["path"].as_str().expect("artifact path")); + let mut tampered = fs::read(&path).expect("read original fixture"); + let original_size = tampered.len(); + tampered[20] ^= 0x01; + fs::write(&path, &tampered).expect("overwrite fixture with same-size content"); + + assert_eq!(tampered.len(), original_size); + let locate_error = runtime + .execute(&envelope( + "artifact.locate", + json!({ "taskId": task_id }), + None, + )) + .expect_err("locate must reject same-size tampering"); + assert_eq!(locate_error.code, "RUNTIME_UNAVAILABLE"); + let read_error = runtime + .read_artifact(task_id) + .expect_err("artifact read must reject same-size tampering"); + assert_eq!(read_error.code, "RUNTIME_UNAVAILABLE"); +} + +#[test] +fn locate_does_not_expose_an_artifact_before_task_completion() { + let workspace = TestWorkspace::create(); + let runtime = ProductionRuntime::open(env!("CARGO_PKG_VERSION"), &workspace.database_path()) + .expect("open Runtime"); + let receipt = runtime + .execute(&envelope( + "runtime.test.delay", + json!({ "delayMs": 400 }), + Some("resolve-fixture-not-ready"), + )) + .expect("submit in-progress task"); + let task_id = receipt["taskId"].as_str().expect("task id"); + let error = runtime + .execute(&envelope( + "artifact.locate", + json!({ "taskId": task_id }), + None, + )) + .expect_err("locate must wait for task completion"); + + assert_eq!(error.code, "TASK_NOT_COMPLETED"); +} diff --git a/tests/flovartRuntimeRegistry.test.ts b/tests/flovartRuntimeRegistry.test.ts index 4e748bc8..3e4c76b0 100644 --- a/tests/flovartRuntimeRegistry.test.ts +++ b/tests/flovartRuntimeRegistry.test.ts @@ -34,16 +34,20 @@ describe('Production Runtime canonical registry', () => { const commandNames = Object.keys(registry.commands); expect(registry.protocolVersion).toBe('1'); - expect(registry.registryHash).toBe('963705f2699d1bef95490314add610ca85ae68b4859e101b1b624f6c51375ff8'); + expect(registry.registryHash).toBe('e2251d5f8e71c1c212a95673104a9d23bc0ed94c5674f964b9baaa0b55cf2f25'); expect(hashCanonicalRegistryDocument(registryDocument)).toBe(registry.registryHash); expect(Object.isFrozen(registry.commands)).toBe(true); expect(Object.isFrozen(registry.commands['runtime.status'].args)).toBe(true); + expect(registry.commands['runtime.test.fixture-image']?.args).toEqual({}); + expect(registry.commands['artifact.locate']?.args).toEqual({ taskId: 'string' }); expect(commandNames).toEqual(expect.arrayContaining([ 'runtime.status', 'command.list', 'command.schema', 'runtime.test.delay', + 'runtime.test.fixture-image', 'task.get', + 'artifact.locate', 'task.inspect', 'task.resume', 'task.list', @@ -51,7 +55,7 @@ describe('Production Runtime canonical registry', () => { 'event.stream', 'workflow.selection.get', ])); - for (const command of ['runtime.test.delay', 'task.get', 'task.inspect', 'task.resume', 'task.list', 'task.cancel', 'event.stream']) { + for (const command of ['runtime.test.delay', 'runtime.test.fixture-image', 'task.get', 'artifact.locate', 'task.inspect', 'task.resume', 'task.list', 'task.cancel', 'event.stream']) { expect(registry.commands[command]?.availability).toBe('available'); } expect(commandNames).not.toContain('workflow.run'); @@ -110,6 +114,7 @@ describe('Production Runtime canonical registry', () => { ])); expect(RUNTIME_WRITE_COMMAND_NAMES).toEqual([ 'runtime.test.delay', + 'runtime.test.fixture-image', 'task.cancel', 'agent-text.route.sync', 'production.dry-run', diff --git a/tools/flovart/contracts/runtime/command-registry.v1.json b/tools/flovart/contracts/runtime/command-registry.v1.json index 8b407da6..728cb48b 100644 --- a/tools/flovart/contracts/runtime/command-registry.v1.json +++ b/tools/flovart/contracts/runtime/command-registry.v1.json @@ -1,7 +1,7 @@ { "schemaVersion": "1", "protocolVersion": "1", - "registryHash": "963705f2699d1bef95490314add610ca85ae68b4859e101b1b624f6c51375ff8", + "registryHash": "e2251d5f8e71c1c212a95673104a9d23bc0ed94c5674f964b9baaa0b55cf2f25", "commands": [ { "name": "runtime.status", @@ -37,6 +37,12 @@ }, "availability": "available" }, + { + "name": "runtime.test.fixture-image", + "summary": "Create a deterministic local PNG fixture task for creative-host import verification without calling a Provider.", + "args": {}, + "availability": "available" + }, { "name": "task.get", "summary": "Return one durable Runtime task by ID.", @@ -45,6 +51,14 @@ }, "availability": "available" }, + { + "name": "artifact.locate", + "summary": "Verify a completed Runtime media artifact and return its durable local path for host import.", + "args": { + "taskId": "string" + }, + "availability": "available" + }, { "name": "task.inspect", "summary": "Return one normalized ProductionTask backed by a durable ProductionRun and its StageRun checkpoint.", diff --git a/tools/flovart/runtime-command-surface.js b/tools/flovart/runtime-command-surface.js index 596d74f2..3490fa96 100644 --- a/tools/flovart/runtime-command-surface.js +++ b/tools/flovart/runtime-command-surface.js @@ -3,7 +3,9 @@ export const RUNTIME_COMMAND_NAMES = Object.freeze([ 'command.list', 'command.schema', 'runtime.test.delay', + 'runtime.test.fixture-image', 'task.get', + 'artifact.locate', 'task.inspect', 'task.resume', 'task.list', @@ -22,6 +24,7 @@ export const RUNTIME_COMMAND_NAMES = Object.freeze([ export const RUNTIME_WRITE_COMMAND_NAMES = Object.freeze([ 'runtime.test.delay', + 'runtime.test.fixture-image', 'task.cancel', 'agent-text.route.sync', 'production.dry-run', From fe8e350c31994ce2bc17971ed859ad21a84b9c31 Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Fri, 25 Sep 2026 20:53:22 +0800 Subject: [PATCH 2/8] fix: complete Resolve media pool output contract --- integrations/studio/shared/inspector.js | 1 + tests/flovartRuntimeTasks.test.ts | 71 +++++++++++++++++++++++++ tests/studioResolveInspector.test.ts | 57 ++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 tests/studioResolveInspector.test.ts diff --git a/integrations/studio/shared/inspector.js b/integrations/studio/shared/inspector.js index e524d0fc..174b7b32 100644 --- a/integrations/studio/shared/inspector.js +++ b/integrations/studio/shared/inspector.js @@ -196,6 +196,7 @@ if (adapter.id === 'premiere') target.append(option('项目素材箱', 'project')); else if (isResolve) target.append(option(t('mediaPool'), 'media-pool')); else if (adapter.id === 'after-effects') target.append(option('当前合成 · 新图层', 'new-layer')); + else if (adapter.id === 'resolve') target.append(option('Media Pool', 'media-pool')); else target.append(option('当前文档 · 新图层', 'new-layer')); target.value = defaultImportTarget.kind; const outputLabel = el('span', isResolve ? t('output') : '添加到'); diff --git a/tests/flovartRuntimeTasks.test.ts b/tests/flovartRuntimeTasks.test.ts index 13aea272..93ade03a 100644 --- a/tests/flovartRuntimeTasks.test.ts +++ b/tests/flovartRuntimeTasks.test.ts @@ -204,6 +204,77 @@ describe('Flovart Runtime tasks', () => { })]); }); + it('routes the no-provider Resolve fixture with its caller-stable idempotency key', async () => { + const { discoveryPath, receipt, requests } = await fixture(); + + const result = await runCli([ + 'runtime.test.fixture-image', + '--idempotency-key', 'resolve-fixture-stable-key', + '--json', + ], { + FLOVART_RUNTIME_DISCOVERY: discoveryPath, + }); + + expect(result.code, JSON.stringify(result)).toBe(0); + expect(result.stderr).toBe(''); + expect(JSON.parse(result.stdout)).toMatchObject({ + ok: true, + command: 'runtime.test.fixture-image', + data: receipt, + runtime: 'production-runtime', + }); + expect(requests).toEqual([expect.objectContaining({ + command: 'runtime.test.fixture-image', + args: {}, + actor: { kind: 'cli', instanceId: 'cli_local' }, + idempotencyKey: 'resolve-fixture-stable-key', + })]); + }); + + it('routes artifact.locate task identity through the Runtime CLI', async () => { + const { discoveryPath, receipt, requests } = await fixture(); + + const result = await runCli([ + 'artifact.locate', + '--task-id', 'task_fixture', + '--json', + ], { + FLOVART_RUNTIME_DISCOVERY: discoveryPath, + }); + + expect(result.code, JSON.stringify(result)).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + ok: true, + command: 'artifact.locate', + data: receipt, + runtime: 'production-runtime', + }); + expect(requests).toEqual([expect.objectContaining({ + command: 'artifact.locate', + args: { taskId: 'task_fixture' }, + actor: { kind: 'cli', instanceId: 'cli_local' }, + })]); + }); + + it('rejects a Resolve fixture command without an idempotency key before Runtime submission', async () => { + const { discoveryPath, requests } = await fixture(); + + const result = await runCli([ + 'runtime.test.fixture-image', + '--json', + ], { + FLOVART_RUNTIME_DISCOVERY: discoveryPath, + }); + + expect(result.code).toBe(1); + expect(JSON.parse(result.stdout)).toMatchObject({ + ok: false, + command: 'runtime.test.fixture-image', + error: { code: 'INVALID_ARGUMENT' }, + }); + expect(requests).toEqual([]); + }); + it('resumes the Runtime SSE ledger with Last-Event-ID', async () => { const { discoveryPath, httpRequests } = await fixture(); const client = new FlovartRuntimeClient({ diff --git a/tests/studioResolveInspector.test.ts b/tests/studioResolveInspector.test.ts new file mode 100644 index 00000000..c61b30f2 --- /dev/null +++ b/tests/studioResolveInspector.test.ts @@ -0,0 +1,57 @@ +import { readFileSync } from 'node:fs'; +import { JSDOM } from 'jsdom'; +import { afterEach, describe, expect, it } from 'vitest'; + +const inspectorSource = readFileSync('integrations/studio/shared/inspector.js', 'utf8'); + +describe('Resolve shared inspector output target', () => { + let dom: JSDOM | undefined; + + afterEach(() => { + dom?.window.close(); + dom = undefined; + }); + + it('exposes, selects, and passes Media Pool as the Resolve output target', async () => { + dom = new JSDOM('
', { runScripts: 'outside-only' }); + dom.window.eval(inspectorSource); + + let receivedTarget: unknown; + const adapter = { + id: 'resolve', + getContext: async () => ({ available: true, documentId: 'project-1', title: 'Resolve project' }), + getSelection: async () => ({ selectionId: 'clip-1', label: 'Interview_A.mov', kind: 'video' }), + subscribeContext: () => ({ dispose() {} }), + }; + const panel = dom.window.FlovartStudioUI.mountInspector({ + root: dom.window.document.getElementById('app'), + adapter, + controller: { + models: [{ label: 'Auto', value: 'auto' }], + async generate(_prompt, target) { + receivedTarget = target; + return { import: { ok: true, message: 'Added' } }; + }, + }, + hostLabel: 'DaVinci Resolve · Clip', + defaultImportTarget: { kind: 'media-pool' }, + }); + + await new Promise(resolve => dom!.window.setTimeout(resolve, 0)); + const output = dom.window.document.querySelector('select[aria-label="输出位置"]'); + expect(output).not.toBeNull(); + expect(Array.from(output!.options).map((item: HTMLOptionElement) => [item.textContent, item.value])).toEqual([ + ['Media Pool', 'media-pool'], + ]); + expect(output!.value).toBe('media-pool'); + + const prompt = dom.window.document.querySelector('#flovart-studio-prompt')!; + prompt.value = 'Create a deterministic test candidate'; + prompt.dispatchEvent(new dom.window.Event('input', { bubbles: true })); + dom.window.document.querySelector('button.fs-generate')!.click(); + await new Promise(resolve => dom!.window.setTimeout(resolve, 0)); + expect(receivedTarget).toEqual({ kind: 'media-pool' }); + + panel.dispose(); + }); +}); From cf178abccf5cda2b4c1b66761809d2cbbb087dad Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Sun, 27 Sep 2026 23:36:30 +0800 Subject: [PATCH 3/8] fix(resolve): avoid duplicate Media Pool option --- integrations/studio/shared/inspector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/integrations/studio/shared/inspector.js b/integrations/studio/shared/inspector.js index 174b7b32..e524d0fc 100644 --- a/integrations/studio/shared/inspector.js +++ b/integrations/studio/shared/inspector.js @@ -196,7 +196,6 @@ if (adapter.id === 'premiere') target.append(option('项目素材箱', 'project')); else if (isResolve) target.append(option(t('mediaPool'), 'media-pool')); else if (adapter.id === 'after-effects') target.append(option('当前合成 · 新图层', 'new-layer')); - else if (adapter.id === 'resolve') target.append(option('Media Pool', 'media-pool')); else target.append(option('当前文档 · 新图层', 'new-layer')); target.value = defaultImportTarget.kind; const outputLabel = el('span', isResolve ? t('output') : '添加到'); From 2b3874d4bcabe8cf1700aaf8d92115438517d675 Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Sun, 27 Sep 2026 23:56:43 +0800 Subject: [PATCH 4/8] docs(resolve): align R1 handoff evidence with current panel --- tests/studioResolveInspector.test.ts | 57 ---------------------------- 1 file changed, 57 deletions(-) delete mode 100644 tests/studioResolveInspector.test.ts diff --git a/tests/studioResolveInspector.test.ts b/tests/studioResolveInspector.test.ts deleted file mode 100644 index c61b30f2..00000000 --- a/tests/studioResolveInspector.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { JSDOM } from 'jsdom'; -import { afterEach, describe, expect, it } from 'vitest'; - -const inspectorSource = readFileSync('integrations/studio/shared/inspector.js', 'utf8'); - -describe('Resolve shared inspector output target', () => { - let dom: JSDOM | undefined; - - afterEach(() => { - dom?.window.close(); - dom = undefined; - }); - - it('exposes, selects, and passes Media Pool as the Resolve output target', async () => { - dom = new JSDOM('
', { runScripts: 'outside-only' }); - dom.window.eval(inspectorSource); - - let receivedTarget: unknown; - const adapter = { - id: 'resolve', - getContext: async () => ({ available: true, documentId: 'project-1', title: 'Resolve project' }), - getSelection: async () => ({ selectionId: 'clip-1', label: 'Interview_A.mov', kind: 'video' }), - subscribeContext: () => ({ dispose() {} }), - }; - const panel = dom.window.FlovartStudioUI.mountInspector({ - root: dom.window.document.getElementById('app'), - adapter, - controller: { - models: [{ label: 'Auto', value: 'auto' }], - async generate(_prompt, target) { - receivedTarget = target; - return { import: { ok: true, message: 'Added' } }; - }, - }, - hostLabel: 'DaVinci Resolve · Clip', - defaultImportTarget: { kind: 'media-pool' }, - }); - - await new Promise(resolve => dom!.window.setTimeout(resolve, 0)); - const output = dom.window.document.querySelector('select[aria-label="输出位置"]'); - expect(output).not.toBeNull(); - expect(Array.from(output!.options).map((item: HTMLOptionElement) => [item.textContent, item.value])).toEqual([ - ['Media Pool', 'media-pool'], - ]); - expect(output!.value).toBe('media-pool'); - - const prompt = dom.window.document.querySelector('#flovart-studio-prompt')!; - prompt.value = 'Create a deterministic test candidate'; - prompt.dispatchEvent(new dom.window.Event('input', { bubbles: true })); - dom.window.document.querySelector('button.fs-generate')!.click(); - await new Promise(resolve => dom!.window.setTimeout(resolve, 0)); - expect(receivedTarget).toEqual({ kind: 'media-pool' }); - - panel.dispose(); - }); -}); From 1300d0054cc8cd349d8a57df9e23a5bc1dd1aebe Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Sun, 27 Sep 2026 23:57:00 +0800 Subject: [PATCH 5/8] docs(resolve): update R1 Runtime handoff status --- docs/content/docs/progress/pending-test.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 21e51ccf..dc2dc8eb 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -9,7 +9,7 @@ title: 待测试确认 - 已实现:`runtime.test.fixture-image` 通过既有 Runtime task ledger 和 Worker,把仓库内固定 `public/favicon.png` 作为 PNG fixture 持久化到 Runtime Artifact 目录;提交要求幂等键,不调用 Provider,任务结果明确记录 `providerCalled: false` 与 `costMicros: 0`。 - 已实现:`artifact.locate --task-id ` 只为 completed task 返回本机持久文件路径;复用 store-relative path containment 校验,并重核文件类型、字节数和 SHA-256。Runtime `read_artifact` 也执行同样的完整性检查;同尺寸内容篡改会失败。 -- 本地验证:`cargo test --manifest-path src-tauri/Cargo.toml --test resolve_r1_artifact -- --nocapture` 通过 3/3,覆盖幂等提交、Runtime 重开后的 Artifact 定位/读取、未完成任务拒绝和同尺寸篡改拒绝;运行时设置 `TAURI_CONFIG={"bundle":{"externalBin":[]}}` 是因为隔离 worktree 没有 Agent sidecar,此次测试没有验证安装包 sidecar。`npx vitest run tests/flovartRuntimeTasks.test.ts tests/flovartRuntimeRegistry.test.ts tests/studioResolveInspector.test.ts` 通过 18/18;`npx tsc --noEmit` 在安装 `dsh-plugin` 锁定依赖后通过;`npm run docs:check` 与 `npm run studio:build` 通过。Studio build 日志仍报告未配置 AE SDK / Resolve WorkflowIntegration.node,这不是 Resolve 宿主验证。 +- 本地验证:`cargo test --manifest-path src-tauri/Cargo.toml --test resolve_r1_artifact -- --nocapture` 通过 3/3,覆盖幂等提交、Runtime 重开后的 Artifact 定位/读取、未完成任务拒绝和同尺寸篡改拒绝;运行时设置 `TAURI_CONFIG={"bundle":{"externalBin":[]}}` 是因为隔离 worktree 没有 Agent sidecar,此次测试没有验证安装包 sidecar。`npx vitest run tests/flovartRuntimeTasks.test.ts tests/flovartRuntimeRegistry.test.ts tests/studioInspector.test.ts` 通过 23/23,沿用已有 Resolve Inspector Media Pool UI 覆盖;`npx tsc --noEmit` 在安装 `dsh-plugin` 锁定依赖后通过;`npm run docs:check` 与 `npm run studio:build` 通过。Studio build 日志仍报告未配置 AE SDK / Resolve WorkflowIntegration.node,这不是 Resolve 宿主验证。 - CLI tracer 顺序:`runtime.test.fixture-image --idempotency-key ` → `task.get --task-id ` → `artifact.locate --task-id `。最后一步只把通过校验的持久本机路径交给同机 Resolve import;不得写进项目元数据或外部服务。 - Flovart canonical Skill 及 `.agents` / `.claude` 镜像记录了 Resolve 21.1 R0/R1 tracer 的目标复核、幂等提交、bounded Media Pool import 与 Timeline 零变化要求;fixture 明确不是根据选中素材生成的 AI 结果。 - 仍待真实宿主验证:Resolve Studio 21.1 native MCP 连接、选中媒体上下文冻结、该路径能否被宿主安全导入 Media Pool、Timeline 保持不变以及重启后素材可用。以上本地 Runtime/CLI 测试不代表 Resolve 导入成功,也没有把 R1 Golden Task 标记完成。 From 993e52c43d161c37b49b2d78593507d7cdc7bcee Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Sun, 27 Sep 2026 23:58:31 +0800 Subject: [PATCH 6/8] docs(resolve): clarify unverified local path handoff --- docs/content/docs/progress/pending-test.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index dc2dc8eb..56e65279 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -10,7 +10,7 @@ title: 待测试确认 - 已实现:`runtime.test.fixture-image` 通过既有 Runtime task ledger 和 Worker,把仓库内固定 `public/favicon.png` 作为 PNG fixture 持久化到 Runtime Artifact 目录;提交要求幂等键,不调用 Provider,任务结果明确记录 `providerCalled: false` 与 `costMicros: 0`。 - 已实现:`artifact.locate --task-id ` 只为 completed task 返回本机持久文件路径;复用 store-relative path containment 校验,并重核文件类型、字节数和 SHA-256。Runtime `read_artifact` 也执行同样的完整性检查;同尺寸内容篡改会失败。 - 本地验证:`cargo test --manifest-path src-tauri/Cargo.toml --test resolve_r1_artifact -- --nocapture` 通过 3/3,覆盖幂等提交、Runtime 重开后的 Artifact 定位/读取、未完成任务拒绝和同尺寸篡改拒绝;运行时设置 `TAURI_CONFIG={"bundle":{"externalBin":[]}}` 是因为隔离 worktree 没有 Agent sidecar,此次测试没有验证安装包 sidecar。`npx vitest run tests/flovartRuntimeTasks.test.ts tests/flovartRuntimeRegistry.test.ts tests/studioInspector.test.ts` 通过 23/23,沿用已有 Resolve Inspector Media Pool UI 覆盖;`npx tsc --noEmit` 在安装 `dsh-plugin` 锁定依赖后通过;`npm run docs:check` 与 `npm run studio:build` 通过。Studio build 日志仍报告未配置 AE SDK / Resolve WorkflowIntegration.node,这不是 Resolve 宿主验证。 -- CLI tracer 顺序:`runtime.test.fixture-image --idempotency-key ` → `task.get --task-id ` → `artifact.locate --task-id `。最后一步只把通过校验的持久本机路径交给同机 Resolve import;不得写进项目元数据或外部服务。 +- Skill 规定的 CLI tracer 顺序:`runtime.test.fixture-image --idempotency-key ` → `task.get --task-id ` → `artifact.locate --task-id `。`artifact.locate` 返回通过校验的本机路径,预期仅临时传给同机 Resolve MCP 的 Media Pool import;当前尚未实际执行或真机验证这一步,不把路径写进项目元数据或外部服务。 - Flovart canonical Skill 及 `.agents` / `.claude` 镜像记录了 Resolve 21.1 R0/R1 tracer 的目标复核、幂等提交、bounded Media Pool import 与 Timeline 零变化要求;fixture 明确不是根据选中素材生成的 AI 结果。 - 仍待真实宿主验证:Resolve Studio 21.1 native MCP 连接、选中媒体上下文冻结、该路径能否被宿主安全导入 Media Pool、Timeline 保持不变以及重启后素材可用。以上本地 Runtime/CLI 测试不代表 Resolve 导入成功,也没有把 R1 Golden Task 标记完成。 @@ -31,7 +31,7 @@ title: 待测试确认 ## 历史基线:UI foundation 首轮(2026-09-25 记录) -> 以下记录保留首轮 UI Foundation 的当时结果,HEAD 未记录;其中 modelFetcher 错误、Settings 英文缺口及对应测试数字均为历史基线,不代表当前 worktree/main 状态。后续 #19 修复了 modelFetcher,#21 合入了 UI Foundation Phase 1;本页“当前 worktree”小节记录本轮状态。 +> 以下记录保留首轮 UI Foundation 的当时结果,HEAD 未记录;其中 modelFetcher 错误、Settings 英文缺口及对应测试数字均为历史基线,不代表当前 main 状态。后续 #19 修复了 modelFetcher,#21 合入了 UI Foundation Phase 1;本页“当前 main”小节记录本轮状态。 ### UI foundation 基础修复与响应式浏览器验收 From 1703a426985668823833cc8ad65b594221e46aea Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Mon, 28 Sep 2026 00:10:38 +0800 Subject: [PATCH 7/8] test(runtime): refresh registry hash assertion --- src-tauri/tests/runtime_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tests/runtime_contract.rs b/src-tauri/tests/runtime_contract.rs index 99bfb773..fd6d7518 100644 --- a/src-tauri/tests/runtime_contract.rs +++ b/src-tauri/tests/runtime_contract.rs @@ -14,7 +14,7 @@ fn runtime_status_comes_from_the_canonical_v1_contract() { assert_eq!(status.state, "ready"); assert_eq!( runtime.registry().registry_hash, - "963705f2699d1bef95490314add610ca85ae68b4859e101b1b624f6c51375ff8" + "e2251d5f8e71c1c212a95673104a9d23bc0ed94c5674f964b9baaa0b55cf2f25" ); assert!(runtime.registry().commands.contains_key("runtime.status")); assert!(!runtime.registry().commands.contains_key("workflow.run")); From dad09778abba35623fefa51c492d539798c76f4d Mon Sep 17 00:00:00 2001 From: bekkilove <921693422@qq.com> Date: Mon, 28 Sep 2026 00:20:52 +0800 Subject: [PATCH 8/8] test(runtime): seed artifact with matching checksum --- src-tauri/tests/runtime_control_server.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/tests/runtime_control_server.rs b/src-tauri/tests/runtime_control_server.rs index fb13f615..c58a0342 100644 --- a/src-tauri/tests/runtime_control_server.rs +++ b/src-tauri/tests/runtime_control_server.rs @@ -1,6 +1,7 @@ use flovart_lib::runtime::{ControlServer, ProductionRuntime}; use rusqlite::{params, Connection}; use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; use std::{ fs, io::{Read, Write}, @@ -442,6 +443,7 @@ fn panel_can_submit_poll_and_download_an_artifact_without_a_browser() { fs::create_dir_all(&images_dir).expect("create images dir"); fs::write(images_dir.join(format!("{task_id}.png")), &artifact_bytes) .expect("write artifact file"); + let artifact_sha256 = hex::encode(Sha256::digest(&artifact_bytes)); let connection = Connection::open(&database_path).expect("open db"); connection .execute( @@ -453,7 +455,7 @@ fn panel_can_submit_poll_and_download_an_artifact_without_a_browser() { "kind": "image", "mimeType": "image/png", "storeRelpath": format!("runtime-artifacts/images/{task_id}.png"), - "sha256": "b".repeat(64), + "sha256": artifact_sha256, "byteSize": artifact_bytes.len() } })