fix(data): canonicalize operation json fields
This commit is contained in:
1 parent
10c1f6d103
commit
b92cf62c4c
10 files changed
+246
-21
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import log from "electron-log/main.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
canonicalOptionalJsonString,
|
||||
jsonStringArraySchema,
|
||||
workProductProviderEnum,
|
||||
workProductReviewStateEnum,
|
||||
workProductStatusEnum,
|
||||
@@ -29,7 +31,7 @@ const createDocumentSchema = z.object({
|
||||
reviewerAgentId: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
goalId: z.string().optional(),
|
||||
tagsJson: z.string().optional(),
|
||||
tagsJson: canonicalOptionalJsonString(jsonStringArraySchema).optional(),
|
||||
});
|
||||
|
||||
const updateDocumentSchema = z.object({
|
||||
@@ -38,7 +40,7 @@ const updateDocumentSchema = z.object({
|
||||
content: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
reviewerAgentId: z.union([z.string(), z.null()]).optional(),
|
||||
tagsJson: z.string().optional(),
|
||||
tagsJson: canonicalOptionalJsonString(jsonStringArraySchema).optional(),
|
||||
changeSummary: z.string().optional(),
|
||||
baseRevisionId: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import log from "electron-log/main.js";
|
||||
import { canonicalJsonString, canonicalOptionalJsonString, jsonArraySchema, jsonStringArraySchema, meetingActionItemArraySchema } from "@shared/contracts";
|
||||
import { z } from "zod";
|
||||
import { validateTaskCreation } from "../task-quality-gates";
|
||||
import { requireParam, type AgentApiServer } from "./server-core";
|
||||
@@ -17,10 +18,10 @@ export function registerMeetingRoutes(server: AgentApiServer) {
|
||||
const meetingCreateSchema = z.object({
|
||||
type: z.string().default("department_sync"),
|
||||
title: z.string().default("Meeting"),
|
||||
participantAgentIds: z.string().default("[]"),
|
||||
participantAgentIds: canonicalJsonString(jsonStringArraySchema, []),
|
||||
scheduledAt: z.string().default(new Date().toISOString()),
|
||||
durationMinutes: z.number().default(30),
|
||||
agendaJson: z.string().default("[]"),
|
||||
agendaJson: canonicalJsonString(jsonArraySchema, []),
|
||||
});
|
||||
|
||||
const parsed = meetingCreateSchema.safeParse(body);
|
||||
@@ -78,9 +79,9 @@ export function registerMeetingRoutes(server: AgentApiServer) {
|
||||
const meetingUpdateSchema = z.object({
|
||||
type: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
notesJson: z.string().optional(),
|
||||
decisionsJson: z.string().optional(),
|
||||
actionItemsJson: z.string().optional(),
|
||||
notesJson: canonicalOptionalJsonString(jsonArraySchema).optional(),
|
||||
decisionsJson: canonicalOptionalJsonString(jsonArraySchema).optional(),
|
||||
actionItemsJson: canonicalOptionalJsonString(meetingActionItemArraySchema).optional(),
|
||||
status: z.string().optional(),
|
||||
scheduledAt: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { canonicalJsonString, jsonStringArraySchema } from "@shared/contracts";
|
||||
import { requireParam, type AgentApiServer } from "./server-core";
|
||||
import { SocialAccountScopeError } from "../database/social";
|
||||
|
||||
@@ -232,7 +233,7 @@ export function registerMiscRoutes(server: AgentApiServer) {
|
||||
topic: z.string().default(""),
|
||||
content: z.string().default(""),
|
||||
importance: z.string().default("medium"),
|
||||
tagsJson: z.string().default("[]"),
|
||||
tagsJson: canonicalJsonString(jsonStringArraySchema, []),
|
||||
referencedEntityType: z.string().nullable().optional(),
|
||||
referencedEntityId: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { canonicalizeJsonString, jsonStringArraySchema } from "@shared/contracts";
|
||||
import type { ReadAll, Transaction } from "./helpers";
|
||||
import { ensureId, nowIso } from "./helpers";
|
||||
import { buildConfigRevisionMethods } from "./config-revisions";
|
||||
@@ -70,14 +71,15 @@ export function buildDocumentMethods(
|
||||
if (existing && existing.companyId !== input.companyId) {
|
||||
throw new Error(`Document ${id} does not belong to company ${input.companyId}`);
|
||||
}
|
||||
const tagsJson = canonicalizeJsonString(input.tagsJson, jsonStringArraySchema, []);
|
||||
if (existing) {
|
||||
getDb().prepare(
|
||||
"update documents set type = ?, title = ?, content = ?, author_agent_id = ?, reviewer_agent_id = ?, project_id = ?, goal_id = ?, parent_doc_id = ?, version = ?, status = ?, tags_json = ?, source_run_id = coalesce(?, source_run_id), updated_at = ? where id = ? and company_id = ?",
|
||||
).run(input.type, input.title, input.content ?? "", input.authorAgentId ?? null, input.reviewerAgentId ?? null, input.projectId ?? null, input.goalId ?? null, input.parentDocId ?? null, input.version ?? 1, input.status ?? "draft", input.tagsJson ?? "[]", input.sourceRunId ?? null, ts, id, input.companyId);
|
||||
).run(input.type, input.title, input.content ?? "", input.authorAgentId ?? null, input.reviewerAgentId ?? null, input.projectId ?? null, input.goalId ?? null, input.parentDocId ?? null, input.version ?? 1, input.status ?? "draft", tagsJson, input.sourceRunId ?? null, ts, id, input.companyId);
|
||||
} else {
|
||||
getDb().prepare(
|
||||
"insert into documents (id, company_id, type, title, content, author_agent_id, reviewer_agent_id, project_id, goal_id, parent_doc_id, version, status, tags_json, source_run_id, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(id, input.companyId, input.type, input.title, input.content ?? "", input.authorAgentId ?? null, input.reviewerAgentId ?? null, input.projectId ?? null, input.goalId ?? null, input.parentDocId ?? null, input.version ?? 1, input.status ?? "draft", input.tagsJson ?? "[]", input.sourceRunId ?? null, ts, ts);
|
||||
).run(id, input.companyId, input.type, input.title, input.content ?? "", input.authorAgentId ?? null, input.reviewerAgentId ?? null, input.projectId ?? null, input.goalId ?? null, input.parentDocId ?? null, input.version ?? 1, input.status ?? "draft", tagsJson, input.sourceRunId ?? null, ts, ts);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { canonicalizeJsonString, jsonArraySchema, jsonStringArraySchema, meetingActionItemArraySchema } from "@shared/contracts";
|
||||
import type { ReadAll } from "./helpers";
|
||||
import { ensureId, nowIso } from "./helpers";
|
||||
|
||||
@@ -101,14 +102,19 @@ export function buildKnowledgeMethods(
|
||||
if (existing && existing.companyId !== input.companyId) {
|
||||
throw new Error(`Meeting ${id} does not belong to company ${input.companyId}`);
|
||||
}
|
||||
const participantAgentIds = canonicalizeJsonString(input.participantAgentIds, jsonStringArraySchema, []);
|
||||
const agendaJson = canonicalizeJsonString(input.agendaJson, jsonArraySchema, []);
|
||||
const notesJson = canonicalizeJsonString(input.notesJson, jsonArraySchema, []);
|
||||
const decisionsJson = canonicalizeJsonString(input.decisionsJson, jsonArraySchema, []);
|
||||
const actionItemsJson = canonicalizeJsonString(input.actionItemsJson, meetingActionItemArraySchema, []);
|
||||
if (existing) {
|
||||
getDb().prepare(
|
||||
"update meetings set type = ?, title = ?, organizer_agent_id = ?, participant_agent_ids = ?, scheduled_at = ?, duration_minutes = ?, agenda_json = ?, notes_json = ?, decisions_json = ?, action_items_json = ?, status = ?, updated_at = ? where id = ? and company_id = ?",
|
||||
).run(input.type, input.title, input.organizerAgentId ?? null, input.participantAgentIds ?? "[]", input.scheduledAt, input.durationMinutes ?? 30, input.agendaJson ?? "[]", input.notesJson ?? "[]", input.decisionsJson ?? "[]", input.actionItemsJson ?? "[]", input.status ?? "scheduled", ts, id, input.companyId);
|
||||
).run(input.type, input.title, input.organizerAgentId ?? null, participantAgentIds, input.scheduledAt, input.durationMinutes ?? 30, agendaJson, notesJson, decisionsJson, actionItemsJson, input.status ?? "scheduled", ts, id, input.companyId);
|
||||
} else {
|
||||
getDb().prepare(
|
||||
"insert into meetings (id, company_id, type, title, organizer_agent_id, participant_agent_ids, scheduled_at, duration_minutes, agenda_json, notes_json, decisions_json, action_items_json, status, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(id, input.companyId, input.type, input.title, input.organizerAgentId ?? null, input.participantAgentIds ?? "[]", input.scheduledAt, input.durationMinutes ?? 30, input.agendaJson ?? "[]", input.notesJson ?? "[]", input.decisionsJson ?? "[]", input.actionItemsJson ?? "[]", input.status ?? "scheduled", ts, ts);
|
||||
).run(id, input.companyId, input.type, input.title, input.organizerAgentId ?? null, participantAgentIds, input.scheduledAt, input.durationMinutes ?? 30, agendaJson, notesJson, decisionsJson, actionItemsJson, input.status ?? "scheduled", ts, ts);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
@@ -131,14 +137,15 @@ export function buildKnowledgeMethods(
|
||||
if (existing && existing.companyId !== input.companyId) {
|
||||
throw new Error(`Knowledge entry ${id} does not belong to company ${input.companyId}`);
|
||||
}
|
||||
const tagsJson = canonicalizeJsonString(input.tagsJson, jsonStringArraySchema, []);
|
||||
if (existing) {
|
||||
getDb().prepare(
|
||||
"update knowledge_base set category = ?, topic = ?, content = ?, author_agent_id = ?, importance = ?, tags_json = ?, referenced_entity_type = ?, referenced_entity_id = ?, source_run_id = coalesce(?, source_run_id), updated_at = ? where id = ? and company_id = ?",
|
||||
).run(input.category, input.topic, input.content, input.authorAgentId ?? null, input.importance ?? "medium", input.tagsJson ?? "[]", input.referencedEntityType ?? null, input.referencedEntityId ?? null, input.sourceRunId ?? null, ts, id, input.companyId);
|
||||
).run(input.category, input.topic, input.content, input.authorAgentId ?? null, input.importance ?? "medium", tagsJson, input.referencedEntityType ?? null, input.referencedEntityId ?? null, input.sourceRunId ?? null, ts, id, input.companyId);
|
||||
} else {
|
||||
getDb().prepare(
|
||||
"insert into knowledge_base (id, company_id, category, topic, content, author_agent_id, importance, tags_json, referenced_entity_type, referenced_entity_id, source_run_id, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(id, input.companyId, input.category, input.topic, input.content, input.authorAgentId ?? null, input.importance ?? "medium", input.tagsJson ?? "[]", input.referencedEntityType ?? null, input.referencedEntityId ?? null, input.sourceRunId ?? null, ts, ts);
|
||||
).run(id, input.companyId, input.category, input.topic, input.content, input.authorAgentId ?? null, input.importance ?? "medium", tagsJson, input.referencedEntityType ?? null, input.referencedEntityId ?? null, input.sourceRunId ?? null, ts, ts);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./contracts/channels";
|
||||
export * from "./contracts/helpers";
|
||||
export * from "./contracts/json-fields";
|
||||
export * from "./contracts/schemas-company";
|
||||
export * from "./contracts/schemas-agents";
|
||||
export * from "./contracts/schemas-tasks";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type JsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() => z.union([
|
||||
z.null(),
|
||||
z.boolean(),
|
||||
z.number(),
|
||||
z.string(),
|
||||
z.array(jsonValueSchema),
|
||||
z.record(z.string(), jsonValueSchema),
|
||||
]));
|
||||
|
||||
export const jsonStringArraySchema = z.array(z.string());
|
||||
export const jsonArraySchema = z.array(jsonValueSchema);
|
||||
|
||||
export const meetingActionItemArraySchema = z.array(z.object({
|
||||
title: z.string().optional(),
|
||||
assigneeAgentId: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
}).catchall(jsonValueSchema));
|
||||
|
||||
export function canonicalizeJsonString<T>(value: string | null | undefined, schema: z.ZodType<T>, fallback: T): string {
|
||||
const raw = value ?? JSON.stringify(fallback);
|
||||
const parsed = JSON.parse(raw);
|
||||
return JSON.stringify(schema.parse(parsed));
|
||||
}
|
||||
|
||||
export function canonicalJsonString<T>(schema: z.ZodType<T>, fallback: T) {
|
||||
const fallbackJson = JSON.stringify(fallback);
|
||||
return z.string().default(fallbackJson).superRefine((value, ctx) => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Must be valid JSON.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = schema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "JSON value does not match the expected shape.",
|
||||
});
|
||||
}
|
||||
}).transform((value) => canonicalizeJsonString(value, schema, fallback));
|
||||
}
|
||||
|
||||
export function canonicalOptionalJsonString<T>(schema: z.ZodType<T>) {
|
||||
return z.string().superRefine((value, ctx) => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Must be valid JSON.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = schema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "JSON value does not match the expected shape.",
|
||||
});
|
||||
}
|
||||
}).transform((value) => JSON.stringify(schema.parse(JSON.parse(value))));
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { canonicalJsonString, jsonArraySchema, jsonStringArraySchema, meetingActionItemArraySchema } from "./json-fields";
|
||||
import { MESSAGE_CHANNELS, optionalId } from "./helpers";
|
||||
|
||||
export const agentMessageInputSchema = z.object({
|
||||
@@ -86,7 +87,7 @@ export const documentInputSchema = z.object({
|
||||
parentDocId: optionalId,
|
||||
version: z.number().int().min(1).default(1),
|
||||
status: z.enum(["draft", "in_review", "approved", "archived", "superseded"]).default("draft"),
|
||||
tagsJson: z.string().default("[]"),
|
||||
tagsJson: canonicalJsonString(jsonStringArraySchema, []),
|
||||
});
|
||||
|
||||
export const knowledgeEntryInputSchema = z.object({
|
||||
@@ -97,7 +98,7 @@ export const knowledgeEntryInputSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
authorAgentId: optionalId,
|
||||
importance: z.enum(["critical", "high", "medium", "low"]).default("medium"),
|
||||
tagsJson: z.string().default("[]"),
|
||||
tagsJson: canonicalJsonString(jsonStringArraySchema, []),
|
||||
referencedEntityType: z.enum([
|
||||
"agents",
|
||||
"goals",
|
||||
@@ -122,13 +123,13 @@ export const meetingInputSchema = z.object({
|
||||
type: z.enum(["standup", "sprint_planning", "sprint_review", "retrospective", "one_on_one", "all_hands", "department_sync", "incident_review", "hiring_committee", "budget_review", "design_review", "architecture_review"]),
|
||||
title: z.string().min(2).max(160),
|
||||
organizerAgentId: optionalId,
|
||||
participantAgentIds: z.string().default("[]"),
|
||||
participantAgentIds: canonicalJsonString(jsonStringArraySchema, []),
|
||||
scheduledAt: z.string(),
|
||||
durationMinutes: z.number().int().min(5).max(480).default(30),
|
||||
agendaJson: z.string().default("[]"),
|
||||
notesJson: z.string().default("[]"),
|
||||
decisionsJson: z.string().default("[]"),
|
||||
actionItemsJson: z.string().default("[]"),
|
||||
agendaJson: canonicalJsonString(jsonArraySchema, []),
|
||||
notesJson: canonicalJsonString(jsonArraySchema, []),
|
||||
decisionsJson: canonicalJsonString(jsonArraySchema, []),
|
||||
actionItemsJson: canonicalJsonString(meetingActionItemArraySchema, []),
|
||||
status: z.enum(["scheduled", "in_progress", "completed", "cancelled"]).default("scheduled"),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
connectorInputSchema,
|
||||
documentInputSchema,
|
||||
knowledgeEntryInputSchema,
|
||||
meetingInputSchema,
|
||||
runLogSchema,
|
||||
secretInputSchema,
|
||||
startRunSchema,
|
||||
@@ -63,4 +66,57 @@ describe("shared contracts", () => {
|
||||
|
||||
expect(parsed.projectId).toBeNull();
|
||||
});
|
||||
|
||||
it("canonicalizes structured JSON string fields", () => {
|
||||
const companyId = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
const document = documentInputSchema.parse({
|
||||
companyId,
|
||||
type: "technical_spec",
|
||||
title: "JSON contract",
|
||||
tagsJson: "[ \"security\", \"audit\" ]",
|
||||
});
|
||||
expect(document.tagsJson).toBe("[\"security\",\"audit\"]");
|
||||
|
||||
const knowledge = knowledgeEntryInputSchema.parse({
|
||||
companyId,
|
||||
category: "technical",
|
||||
topic: "JSON fields",
|
||||
content: "Canonicalize JSON before persistence.",
|
||||
tagsJson: "[ \"runtime\" ]",
|
||||
});
|
||||
expect(knowledge.tagsJson).toBe("[\"runtime\"]");
|
||||
|
||||
const meeting = meetingInputSchema.parse({
|
||||
companyId,
|
||||
type: "department_sync",
|
||||
title: "JSON field sync",
|
||||
scheduledAt: "2026-05-15T10:00:00.000Z",
|
||||
participantAgentIds: "[ \"11111111-1111-1111-1111-111111111111\" ]",
|
||||
agendaJson: "[ \"Plan\" ]",
|
||||
actionItemsJson: "[ { \"title\": \"Ship\", \"description\": \"Finish\" } ]",
|
||||
});
|
||||
expect(meeting.participantAgentIds).toBe("[\"11111111-1111-1111-1111-111111111111\"]");
|
||||
expect(meeting.agendaJson).toBe("[\"Plan\"]");
|
||||
expect(meeting.actionItemsJson).toBe("[{\"title\":\"Ship\",\"description\":\"Finish\"}]");
|
||||
});
|
||||
|
||||
it("rejects malformed structured JSON string fields", () => {
|
||||
const companyId = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
expect(() => documentInputSchema.parse({
|
||||
companyId,
|
||||
type: "technical_spec",
|
||||
title: "Broken tags",
|
||||
tagsJson: "{",
|
||||
})).toThrow();
|
||||
|
||||
expect(() => meetingInputSchema.parse({
|
||||
companyId,
|
||||
type: "department_sync",
|
||||
title: "Broken participants",
|
||||
scheduledAt: "2026-05-15T10:00:00.000Z",
|
||||
participantAgentIds: "[1]",
|
||||
})).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.AGENTCOMPANY_ALLOW_INSECURE_VAULT = "true";
|
||||
});
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: () => false,
|
||||
getSelectedStorageBackend: () => "basic_text",
|
||||
encryptString: (value: string) => Buffer.from(value, "utf8"),
|
||||
decryptString: (value: Buffer) => value.toString("utf8"),
|
||||
},
|
||||
}));
|
||||
|
||||
import { AppDatabase } from "../../src/main/database";
|
||||
|
||||
describe("operation JSON fields", () => {
|
||||
let tempDir: string;
|
||||
let db: AppDatabase;
|
||||
let companyId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-json-fields-"));
|
||||
db = new AppDatabase(tempDir);
|
||||
db.init();
|
||||
companyId = db.core.company.saveCompany({
|
||||
name: "JSON Field Co",
|
||||
description: "JSON field validation",
|
||||
status: "active",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("canonicalizes document and knowledge tag JSON at the DAO boundary", () => {
|
||||
const documentId = db.core.documents.saveDocument({
|
||||
companyId,
|
||||
type: "technical_spec",
|
||||
title: "Canonical tags",
|
||||
tagsJson: "[ \"security\", \"runtime\" ]",
|
||||
});
|
||||
const document = db.core.documents.getDocumentForUpdate(documentId, companyId);
|
||||
expect(document?.tagsJson).toBe("[\"security\",\"runtime\"]");
|
||||
|
||||
const knowledgeId = db.core.knowledge.saveKnowledgeEntry({
|
||||
companyId,
|
||||
category: "technical",
|
||||
topic: "Canonical knowledge tags",
|
||||
content: "Persisted JSON is canonical.",
|
||||
tagsJson: "[ \"audit\" ]",
|
||||
});
|
||||
const knowledge = db.core.knowledge.listKnowledgeEntries(companyId).find((entry) => entry.id === knowledgeId);
|
||||
expect(knowledge?.tagsJson).toBe("[\"audit\"]");
|
||||
});
|
||||
|
||||
it("rejects malformed meeting JSON before writing", () => {
|
||||
expect(() => db.core.knowledge.saveMeeting({
|
||||
companyId,
|
||||
type: "department_sync",
|
||||
title: "Broken JSON",
|
||||
scheduledAt: "2026-05-15T10:00:00.000Z",
|
||||
participantAgentIds: "[1]",
|
||||
})).toThrow();
|
||||
|
||||
expect(db.core.knowledge.listMeetings(companyId)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user