fix(spa): extract 8hex UUID suffix from module DB id for @{uuid} refs

Bug A (`@{name}` → `@{uuid}`) shipped with a follow-on issue: I made
`wcSuggestions` return `m.id` (the full DB id like `wc_test2_ea67b173`),
but the syntax spec locks the wildcard ref form to `@{8hex}` only and
the tokenizer regex (`@\{[0-9a-f]{8}\}`) rejects anything else. So the
textarea ended up with raw `@{wc_test2_ea67b173}` text, no chip.

`ModuleRepository._gen_id` on the Python side builds ids as
`<prefix>_<slug>_<token_hex(4)>` — the trailing 8 hex chars are the
canonical UUID. This commit:

  - Adds `extractModuleUuid(id)` helper in src/manager/utils/slug.ts
    that returns the suffix or null for non-conforming ids
  - Switches `WildcardEditor.wcSuggestions` and `nameByUuid` to key
    by the 8hex UUID, not the full DB id
  - Pins behaviour with 3 new tests on the helper

Runtime catalog population (`__wp_catalog__` is currently `{}` in
`wp_api/test_runner.py`) is a separate issue tracked under the upcoming
ComfyUI-bridge brainstorm; this commit only fixes the SPA-side syntax
correctness.
This commit is contained in:
DumiFlex
2026-04-28 10:17:08 +03:00
parent 89c16624db
commit 3d18829e54
4 changed files with 67 additions and 12 deletions
+23 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
import { extractModuleUuid, toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
describe("toIdentifier", () => {
it("lowercases and slug-joins runs of non-identifier chars", () => {
@@ -34,3 +34,25 @@ describe("toIdentifier", () => {
}
});
});
describe("extractModuleUuid", () => {
it("returns the trailing 8-hex suffix from a normal DB id", () => {
expect(extractModuleUuid("wc_test2_ea67b173")).toBe("ea67b173");
expect(extractModuleUuid("co_subject_aabbccdd")).toBe("aabbccdd");
expect(extractModuleUuid("fv_my_consts_00112233")).toBe("00112233");
});
it("accepts ids whose slug section contains digits and underscores", () => {
expect(extractModuleUuid("wc_test_2_ea67b173")).toBe("ea67b173");
expect(extractModuleUuid("wc_a_b_c_d_e_deadbeef")).toBe("deadbeef");
});
it("returns null when the id does not end in `_<8 lowercase hex>`", () => {
expect(extractModuleUuid("wc_test2")).toBeNull();
expect(extractModuleUuid("wc_test2_EA67B173")).toBeNull(); // uppercase rejected
expect(extractModuleUuid("wc_test2_eaa67b173")).toBeNull(); // 9 chars
expect(extractModuleUuid("wc_test2_ea67b17")).toBeNull(); // 7 chars
expect(extractModuleUuid("ea67b173")).toBeNull(); // missing underscore prefix
expect(extractModuleUuid("")).toBeNull();
});
});
+20
View File
@@ -19,3 +19,23 @@ export function toIdentifier(input: string): string {
}
export const VALID_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
/**
* Extract the canonical 8-hex wildcard UUID from a full module DB id.
*
* `ModuleRepository._gen_id` builds ids as `<prefix>_<slug>_<token_hex(4)>`,
* e.g. `wc_test2_ea67b173`. The syntax spec (§2.4 in 2026-04-28 design doc)
* locks the wildcard ref form to `@{8hex}` — the trailing 8 hex chars are
* the UUID. SPA editors that surface refs in autocomplete must extract this
* suffix instead of inserting the whole DB id verbatim, otherwise the
* tokenizer regex (matching `@\{[0-9a-f]{8}\}`) rejects the token and the
* user sees raw `@{wc_test2_ea67b173}` text in the textarea with no chip.
*
* Returns null when the id does not end in `_<8hex>` (legacy/fixture data,
* imported bundles with non-standard ids, etc.) so callers can decide
* whether to skip or fall back.
*/
export function extractModuleUuid(id: string): string | null {
const m = id.match(/_([0-9a-f]{8})$/);
return m ? m[1] : null;
}
+23 -10
View File
@@ -23,7 +23,7 @@ import RichTextInput from "../components/RichTextInput.vue";
import { useToast } from "../composables/useToast";
import { useModuleStore } from "../stores/moduleStore";
import { useCategoryStore } from "../stores/categoryStore";
import { toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
import { extractModuleUuid, toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
import { appendSnapshot, readHistory } from "../utils/history";
import type {
CombinePayload,
@@ -54,18 +54,27 @@ const saving = ref(false);
const isEdit = computed(() => !!props.id);
const historyEntries = ref<ModuleHistoryEntry[]>([]);
// Suggestions: every other wildcard's UUID (excluding self) for `@`-trigger
// nested-reference autocomplete. The canonical stored form is `@{8hex}` per
// the syntax spec — the popover surfaces the human display name (via
// `uuidToName` below) but the inserted token is the UUID. Returning names
// here was the bug that caused `@{module_name}` to appear in the textarea
// instead of `@{a1b2c3d4}`.
// Suggestions: every other wildcard's 8-hex UUID (excluding self) for the
// `@`-trigger nested-reference autocomplete. The canonical stored form is
// `@{8hex}` per the syntax spec — the popover surfaces the human display
// name (via `nameByUuid` below) but the inserted token is the bare UUID.
//
// Module DB ids are `<prefix>_<slug>_<8hex>` (see `ModuleRepository._gen_id`).
// We strip everything except the trailing 8 hex chars: that is what the
// tokenizer regex `@\{[0-9a-f]{8}\}` accepts, and it matches the runtime
// catalog key shape consumed by `engine.modules.get_module(uuid)`.
//
// Two earlier shapes were wrong:
// 1. var_binding/name → produced `@{test2}` (caught by users)
// 2. full DB id (e.g. `wc_test2_ea67b173`) → tokenizer rejected it and the
// textarea showed raw `@{wc_test2_ea67b173}` text with no ref chip
const wcSuggestions = computed<string[]>(() => {
const out: string[] = [];
for (const m of moduleStore.items) {
if (m.type !== "wildcard") continue;
if (props.id && m.id === props.id) continue;
out.push(m.id);
const uuid = extractModuleUuid(m.id);
if (uuid) out.push(uuid);
}
// Sort by display name so the popover orders alphabetically by what the
// user actually sees, not by UUID hex.
@@ -77,14 +86,18 @@ const wcSuggestions = computed<string[]>(() => {
});
// UUID → display-name map used by RichTextInput to render `@{uuid}` chips
// and the `@`-trigger autocomplete popover with human labels.
// and the `@`-trigger autocomplete popover with human labels. Keyed by the
// same 8-hex UUID surfaced through `wcSuggestions` so the popover, the
// inline chip, and the resolver all agree on identity.
const nameByUuid = computed<Map<string, string>>(() => {
const m = new Map<string, string>();
for (const mod of moduleStore.items) {
if (mod.type !== "wildcard") continue;
const uuid = extractModuleUuid(mod.id);
if (!uuid) continue;
const p = (mod.payload ?? {}) as { var_binding?: string };
const display = (p.var_binding && p.var_binding.trim()) || toIdentifier(mod.name);
if (display) m.set(mod.id, display);
if (display) m.set(uuid, display);
}
return m;
});
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
<link rel="manifest" href="/wp/images/site.webmanifest" />
<script type="module" crossorigin src="/wp/assets/manager-DiDV9tbM.js"></script>
<script type="module" crossorigin src="/wp/assets/manager-yLWQMHbh.js"></script>
<link rel="stylesheet" crossorigin href="/wp/assets/manager-Dgq4_qmM.css">
</head>
<body>