fix(wiki): make built-in transformation starter pack visible in every workspace

This commit is contained in:
matevip 2026-06-29 14:35:09 +08:00
parent 64b5587f56
commit 07d6f01b56
6 changed files with 195 additions and 6 deletions

View File

@ -145,7 +145,10 @@ public class WikiTransformationExecutor {
WikiTransformationRunEntity run = new WikiTransformationRunEntity();
run.setTransformationId(transformation.getId());
run.setKbId(page.getKbId());
run.setWorkspaceId(transformation.getWorkspaceId());
// Global templates have a null workspace_id; the run row requires one, so
// fall back to the default workspace for bookkeeping (run visibility is
// gated by the KB's workspace, not this field).
run.setWorkspaceId(transformation.getWorkspaceId() != null ? transformation.getWorkspaceId() : 1L);
run.setInputKind("page");
run.setPageId(pageId);
run.setStatus("running");
@ -420,7 +423,10 @@ public class WikiTransformationExecutor {
WikiTransformationRunEntity run = new WikiTransformationRunEntity();
run.setTransformationId(transformation.getId());
run.setKbId(raw.getKbId());
run.setWorkspaceId(transformation.getWorkspaceId());
// Global templates have a null workspace_id; the run row requires one, so
// fall back to the default workspace for bookkeeping (run visibility is
// gated by the KB's workspace, not this field).
run.setWorkspaceId(transformation.getWorkspaceId() != null ? transformation.getWorkspaceId() : 1L);
run.setInputKind("raw");
run.setRawId(rawId);
run.setStatus("running");

View File

@ -29,7 +29,12 @@ public class WikiTransformationService {
private final WikiTransformationMapper transformationMapper;
private final WikiTransformationRunMapper runMapper;
/** Templates visible to a KB: pinned to this KB plus workspace-wide ones (kb_id NULL). */
/**
* Templates visible to a KB: pinned to this KB, plus workspace-wide ones
* either scoped to this workspace ({@code workspace_id = current}) or global
* ({@code workspace_id IS NULL}, e.g. the built-in starter pack, visible to
* every workspace).
*/
public List<WikiTransformationEntity> listForKb(Long kbId, Long workspaceId) {
if (kbId == null) {
return List.of();
@ -38,14 +43,17 @@ public class WikiTransformationService {
new LambdaQueryWrapper<WikiTransformationEntity>()
.and(w -> w.eq(WikiTransformationEntity::getKbId, kbId)
.or(g -> g.isNull(WikiTransformationEntity::getKbId)
.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)))
.and(ws -> ws.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)
.or().isNull(WikiTransformationEntity::getWorkspaceId))))
.orderByDesc(WikiTransformationEntity::getUpdateTime));
}
public List<WikiTransformationEntity> listByWorkspace(Long workspaceId) {
return transformationMapper.selectList(
new LambdaQueryWrapper<WikiTransformationEntity>()
.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)
// This workspace's own templates plus global ones (workspace_id NULL).
.and(w -> w.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)
.or().isNull(WikiTransformationEntity::getWorkspaceId))
.orderByDesc(WikiTransformationEntity::getUpdateTime));
}
@ -65,7 +73,9 @@ public class WikiTransformationService {
WikiTransformationEntity global = transformationMapper.selectOne(
new LambdaQueryWrapper<WikiTransformationEntity>()
.isNull(WikiTransformationEntity::getKbId)
.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)
// This workspace's own template, or a global one (workspace_id NULL).
.and(ws -> ws.eq(WikiTransformationEntity::getWorkspaceId, workspaceId)
.or().isNull(WikiTransformationEntity::getWorkspaceId))
.eq(WikiTransformationEntity::getName, name)
.last("LIMIT 1"));
return Optional.ofNullable(global);

View File

@ -0,0 +1,14 @@
-- V165: make the built-in starter-pack transformation templates global.
-- V108 seeded the 7 templates with a hardcoded workspace_id = 1, so any
-- workspace other than 1 saw an empty Transformations list. The fix marks them
-- global by clearing workspace_id (NULL = global). The column was NOT NULL, so
-- relax it first; listForKb / listByWorkspace / findByName treat NULL as global,
-- and the access checks already allow templates whose workspace_id IS NULL.
-- Targeted by fixed seed ids so real user templates are untouched.
ALTER TABLE mate_wiki_transformation ALTER COLUMN workspace_id SET NULL;
UPDATE mate_wiki_transformation
SET workspace_id = NULL
WHERE id IN (1000004001, 1000004002, 1000004003, 1000004004, 1000004005, 1000004006, 1000004007)
AND workspace_id = 1;

View File

@ -0,0 +1,14 @@
-- V165: make the built-in starter-pack transformation templates global.
-- V108 seeded the 7 templates with a hardcoded workspace_id = 1, so any
-- workspace other than 1 saw an empty Transformations list. The fix marks them
-- global by clearing workspace_id (NULL = global). The column was NOT NULL, so
-- relax it first; listForKb / listByWorkspace / findByName treat NULL as global,
-- and the access checks already allow templates whose workspace_id IS NULL.
-- Targeted by fixed seed ids so real user templates are untouched.
ALTER TABLE mate_wiki_transformation ALTER COLUMN workspace_id DROP NOT NULL;
UPDATE mate_wiki_transformation
SET workspace_id = NULL
WHERE id IN (1000004001, 1000004002, 1000004003, 1000004004, 1000004005, 1000004006, 1000004007)
AND workspace_id = 1;

View File

@ -0,0 +1,14 @@
-- V165: make the built-in starter-pack transformation templates global.
-- V108 seeded the 7 templates with a hardcoded workspace_id = 1, so any
-- workspace other than 1 saw an empty Transformations list. The fix marks them
-- global by clearing workspace_id (NULL = global). The column was NOT NULL, so
-- relax it first; listForKb / listByWorkspace / findByName treat NULL as global,
-- and the access checks already allow templates whose workspace_id IS NULL.
-- Targeted by fixed seed ids so real user templates are untouched.
ALTER TABLE mate_wiki_transformation MODIFY COLUMN workspace_id BIGINT NULL DEFAULT 1;
UPDATE mate_wiki_transformation
SET workspace_id = NULL
WHERE id IN (1000004001, 1000004002, 1000004003, 1000004004, 1000004005, 1000004006, 1000004007)
AND workspace_id = 1;

View File

@ -0,0 +1,131 @@
package vip.mate.wiki.service;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.model.WikiTransformationEntity;
import vip.mate.wiki.repository.WikiKnowledgeBaseMapper;
import vip.mate.wiki.repository.WikiTransformationMapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Regression for the starter-pack visibility bug: the 7 built-in transformation
* templates were seeded with a hardcoded {@code workspace_id = 1}, so any other
* workspace saw an empty Transformations list. V165 clears their workspace_id
* (NULL = global) and the queries treat NULL as visible everywhere this test
* boots H2 with the real Flyway migrations (V108 seed + V165 fix) and asserts
* the templates show up regardless of workspace, while workspace-scoped
* templates stay isolated.
*/
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"spring.flyway.enabled=true",
"spring.flyway.locations=classpath:db/migration/h2",
"mateclaw.feature-flag.refresh-ms=999999"
}
)
class WikiTransformationStarterPackGlobalE2ETest {
private static final Set<String> STARTER_PACK = Set.of(
"contract-risk-extract", "meeting-action-items", "customer-profile",
"competitor-update", "resume-structured-extract", "incident-postmortem", "paper-imrad");
private static final AtomicLong SEQ = new AtomicLong(System.nanoTime());
@Autowired
private WikiTransformationService service;
@Autowired
private WikiKnowledgeBaseMapper kbMapper;
@Autowired
private WikiTransformationMapper transformationMapper;
private long newKb(long workspaceId) {
WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity();
long id = SEQ.incrementAndGet();
kb.setId(id);
kb.setName("kb-" + id);
kb.setStatus("active");
kb.setWorkspaceId(workspaceId);
kb.setCreateTime(LocalDateTime.now());
kb.setUpdateTime(LocalDateTime.now());
kb.setDeleted(0);
kbMapper.insert(kb);
return id;
}
private void insertWorkspaceTemplate(long workspaceId, String name) {
WikiTransformationEntity t = new WikiTransformationEntity();
t.setId(SEQ.incrementAndGet());
t.setKbId(null); // workspace-wide
t.setWorkspaceId(workspaceId); // but scoped to one workspace
t.setName(name);
t.setTitle(name);
t.setPromptTemplate("do something");
t.setEnabled(true);
t.setCreateTime(LocalDateTime.now());
t.setUpdateTime(LocalDateTime.now());
t.setDeleted(0);
transformationMapper.insert(t);
}
private Set<String> visibleNames(long kbId, long workspaceId) {
return service.listForKb(kbId, workspaceId).stream()
.map(WikiTransformationEntity::getName)
.collect(Collectors.toSet());
}
@Test
@DisplayName("Starter pack is visible from a non-default workspace (the bug)")
void starterPackVisibleFromOtherWorkspace() {
long kb = newKb(999L);
Set<String> names = visibleNames(kb, 999L);
for (String expected : STARTER_PACK) {
assertTrue(names.contains(expected),
"workspace 999 should see starter-pack template '" + expected + "', got: " + names);
}
}
@Test
@DisplayName("Starter pack still visible from workspace 1 (no regression)")
void starterPackStillVisibleFromWorkspaceOne() {
long kb = newKb(1L);
assertTrue(visibleNames(kb, 1L).containsAll(STARTER_PACK));
}
@Test
@DisplayName("A workspace-scoped template stays isolated; globals are seen by both")
void workspaceScopedTemplateStaysIsolated() {
String unique = "ws-only-" + SEQ.incrementAndGet();
insertWorkspaceTemplate(777L, unique);
long kb777 = newKb(777L);
long kb888 = newKb(888L);
assertTrue(visibleNames(kb777, 777L).contains(unique), "owner workspace should see its template");
assertFalse(visibleNames(kb888, 888L).contains(unique), "other workspace must NOT see it");
// Global starter pack reaches both workspaces.
assertTrue(visibleNames(kb777, 777L).containsAll(STARTER_PACK));
assertTrue(visibleNames(kb888, 888L).containsAll(STARTER_PACK));
}
@Test
@DisplayName("listByWorkspace surfaces globals alongside the workspace's own")
void listByWorkspaceIncludesGlobals() {
List<WikiTransformationEntity> rows = service.listByWorkspace(424242L);
Set<String> names = rows.stream().map(WikiTransformationEntity::getName).collect(Collectors.toSet());
assertTrue(names.containsAll(STARTER_PACK),
"listByWorkspace for an arbitrary workspace should still include the global starter pack");
}
}