@apart-tech/intelligence-core 1.11.5 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/auth/ability.d.ts +4 -4
  2. package/dist/auth/ability.d.ts.map +1 -1
  3. package/dist/auth/ability.js +17 -11
  4. package/dist/auth/ability.js.map +1 -1
  5. package/dist/auth/ability.test.js +25 -12
  6. package/dist/auth/ability.test.js.map +1 -1
  7. package/dist/config/index.d.ts.map +1 -1
  8. package/dist/config/index.js +5 -1
  9. package/dist/config/index.js.map +1 -1
  10. package/dist/db/tenant.d.ts.map +1 -1
  11. package/dist/db/tenant.js +8 -0
  12. package/dist/db/tenant.js.map +1 -1
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/services/__tests__/chunk-service.test.d.ts +2 -0
  18. package/dist/services/__tests__/chunk-service.test.d.ts.map +1 -0
  19. package/dist/services/__tests__/chunk-service.test.js +111 -0
  20. package/dist/services/__tests__/chunk-service.test.js.map +1 -0
  21. package/dist/services/__tests__/chunker.test.d.ts +2 -0
  22. package/dist/services/__tests__/chunker.test.d.ts.map +1 -0
  23. package/dist/services/__tests__/chunker.test.js +113 -0
  24. package/dist/services/__tests__/chunker.test.js.map +1 -0
  25. package/dist/services/__tests__/node-service.test.d.ts +2 -0
  26. package/dist/services/__tests__/node-service.test.d.ts.map +1 -0
  27. package/dist/services/__tests__/node-service.test.js +207 -0
  28. package/dist/services/__tests__/node-service.test.js.map +1 -0
  29. package/dist/services/__tests__/pii-detector-service.test.js +51 -0
  30. package/dist/services/__tests__/pii-detector-service.test.js.map +1 -1
  31. package/dist/services/__tests__/pii-encryption-service.test.js +37 -0
  32. package/dist/services/__tests__/pii-encryption-service.test.js.map +1 -1
  33. package/dist/services/__tests__/search-service.test.d.ts +2 -0
  34. package/dist/services/__tests__/search-service.test.d.ts.map +1 -0
  35. package/dist/services/__tests__/search-service.test.js +163 -0
  36. package/dist/services/__tests__/search-service.test.js.map +1 -0
  37. package/dist/services/backfill-chunks.d.ts +30 -0
  38. package/dist/services/backfill-chunks.d.ts.map +1 -0
  39. package/dist/services/backfill-chunks.js +55 -0
  40. package/dist/services/backfill-chunks.js.map +1 -0
  41. package/dist/services/chunk-service.d.ts +45 -0
  42. package/dist/services/chunk-service.d.ts.map +1 -0
  43. package/dist/services/chunk-service.js +111 -0
  44. package/dist/services/chunk-service.js.map +1 -0
  45. package/dist/services/chunker.d.ts +32 -0
  46. package/dist/services/chunker.d.ts.map +1 -0
  47. package/dist/services/chunker.js +289 -0
  48. package/dist/services/chunker.js.map +1 -0
  49. package/dist/services/context-service.d.ts +3 -1
  50. package/dist/services/context-service.d.ts.map +1 -1
  51. package/dist/services/context-service.js +17 -1
  52. package/dist/services/context-service.js.map +1 -1
  53. package/dist/services/node-service.d.ts +12 -1
  54. package/dist/services/node-service.d.ts.map +1 -1
  55. package/dist/services/node-service.js +54 -11
  56. package/dist/services/node-service.js.map +1 -1
  57. package/dist/services/pii-detector-service.d.ts +1 -0
  58. package/dist/services/pii-detector-service.d.ts.map +1 -1
  59. package/dist/services/pii-detector-service.js +95 -2
  60. package/dist/services/pii-detector-service.js.map +1 -1
  61. package/dist/services/pii-encryption-service.d.ts +10 -0
  62. package/dist/services/pii-encryption-service.d.ts.map +1 -1
  63. package/dist/services/pii-encryption-service.js +32 -0
  64. package/dist/services/pii-encryption-service.js.map +1 -1
  65. package/dist/services/search-service.d.ts +30 -1
  66. package/dist/services/search-service.d.ts.map +1 -1
  67. package/dist/services/search-service.js +262 -45
  68. package/dist/services/search-service.js.map +1 -1
  69. package/dist/services/tag-service.d.ts +78 -0
  70. package/dist/services/tag-service.d.ts.map +1 -0
  71. package/dist/services/tag-service.js +639 -0
  72. package/dist/services/tag-service.js.map +1 -0
  73. package/dist/services/tag-service.test.d.ts +2 -0
  74. package/dist/services/tag-service.test.d.ts.map +1 -0
  75. package/dist/services/tag-service.test.js +448 -0
  76. package/dist/services/tag-service.test.js.map +1 -0
  77. package/dist/services/workspace-service.d.ts +2 -0
  78. package/dist/services/workspace-service.d.ts.map +1 -1
  79. package/dist/services/workspace-service.js +7 -1
  80. package/dist/services/workspace-service.js.map +1 -1
  81. package/dist/types/index.d.ts +67 -2
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/package.json +2 -2
  84. package/prisma/schema.prisma +180 -3
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ChunkService } from "../chunk-service.js";
3
+ import { PiiDetectorService } from "../pii-detector-service.js";
4
+ import { PiiEncryptionService } from "../pii-encryption-service.js";
5
+ function createMockDb() {
6
+ const rawCalls = [];
7
+ return {
8
+ $executeRaw: vi.fn((...args) => {
9
+ rawCalls.push(args);
10
+ return Promise.resolve(1);
11
+ }),
12
+ $queryRaw: vi.fn().mockResolvedValue([]),
13
+ $queryRawUnsafe: vi.fn().mockResolvedValue([]),
14
+ _rawCalls: rawCalls,
15
+ };
16
+ }
17
+ function createMockEmbeddings() {
18
+ return {
19
+ lastBatchInput: null,
20
+ embed: vi.fn(async () => [0.1, 0.2, 0.3]),
21
+ embedBatch: vi.fn(async function (texts) {
22
+ this.lastBatchInput = texts;
23
+ return texts.map(() => [0.1, 0.2, 0.3]);
24
+ }),
25
+ };
26
+ }
27
+ describe("ChunkService", () => {
28
+ const detector = new PiiDetectorService();
29
+ const piiEncryption = new PiiEncryptionService(detector);
30
+ const tenantCtx = { organizationId: "org-1" };
31
+ let db;
32
+ let embeddings;
33
+ beforeEach(() => {
34
+ db = createMockDb();
35
+ embeddings = createMockEmbeddings();
36
+ });
37
+ describe("chunkAndEmbed", () => {
38
+ it("calls embedBatch with heading-prefixed texts", async () => {
39
+ const service = new ChunkService(db, embeddings, tenantCtx);
40
+ const content = "# Overview\n\nThis is a paragraph about the overview topic.";
41
+ await service.chunkAndEmbed("node-1", "Test Title", content);
42
+ expect(embeddings.embedBatch).toHaveBeenCalled();
43
+ const batchTexts = embeddings.embedBatch.mock.calls[0][0];
44
+ // At least one text should have heading prefix
45
+ expect(batchTexts.some((t) => t.includes("Overview:"))).toBe(true);
46
+ });
47
+ it("deletes existing chunks before inserting", async () => {
48
+ const service = new ChunkService(db, embeddings, tenantCtx);
49
+ await service.chunkAndEmbed("node-1", "Title", "Some content here.");
50
+ // $executeRaw should be called for DELETE
51
+ expect(db.$executeRaw).toHaveBeenCalled();
52
+ const deleteCall = db.$executeRaw.mock.calls.find((call) => {
53
+ const tpl = call[0];
54
+ return tpl.raw.join("").includes("DELETE FROM node_chunks");
55
+ });
56
+ expect(deleteCall).toBeDefined();
57
+ });
58
+ it("strips PII before chunking in encrypt mode", async () => {
59
+ const service = new ChunkService(db, embeddings, tenantCtx, piiEncryption, { mode: "encrypt", orgKey: Buffer.alloc(32) });
60
+ await service.chunkAndEmbed("node-1", "Contact", "Email john@example.com for info");
61
+ expect(embeddings.embedBatch).toHaveBeenCalled();
62
+ const batchTexts = embeddings.embedBatch.mock.calls[0][0];
63
+ // PII should be stripped
64
+ for (const text of batchTexts) {
65
+ expect(text).not.toContain("john@example.com");
66
+ expect(text).toContain("[EMAIL]");
67
+ }
68
+ });
69
+ it("sets embedding_pii_mode on chunks", async () => {
70
+ const service = new ChunkService(db, embeddings, tenantCtx, piiEncryption, { mode: "encrypt", orgKey: Buffer.alloc(32) });
71
+ await service.chunkAndEmbed("node-1", "Title", "Some content for testing.");
72
+ // $queryRawUnsafe should be called with INSERT containing pii mode
73
+ expect(db.$queryRawUnsafe).toHaveBeenCalled();
74
+ const insertCall = db.$queryRawUnsafe.mock.calls[0];
75
+ const sql = insertCall[0];
76
+ expect(sql).toContain("INSERT INTO node_chunks");
77
+ // One of the params should be the pii mode
78
+ expect(insertCall).toContain("encrypt");
79
+ });
80
+ it("does nothing for empty content", async () => {
81
+ const service = new ChunkService(db, embeddings, tenantCtx);
82
+ await service.chunkAndEmbed("node-1", "", "");
83
+ expect(embeddings.embedBatch).not.toHaveBeenCalled();
84
+ expect(db.$queryRawUnsafe).not.toHaveBeenCalled();
85
+ });
86
+ });
87
+ describe("deleteByNodeId", () => {
88
+ it("executes DELETE via $executeRaw", async () => {
89
+ const service = new ChunkService(db, embeddings, tenantCtx);
90
+ await service.deleteByNodeId("node-1");
91
+ expect(db.$executeRaw).toHaveBeenCalled();
92
+ const call = db.$executeRaw.mock.calls[0];
93
+ const tpl = call[0];
94
+ const sql = tpl.raw.join("");
95
+ expect(sql).toContain("DELETE FROM node_chunks");
96
+ });
97
+ });
98
+ describe("getByNodeId", () => {
99
+ it("queries with tenant filter and orders by chunk_index", async () => {
100
+ const service = new ChunkService(db, embeddings, tenantCtx);
101
+ await service.getByNodeId("node-1");
102
+ expect(db.$queryRaw).toHaveBeenCalled();
103
+ const call = db.$queryRaw.mock.calls[0];
104
+ const tpl = call[0];
105
+ const sql = tpl.raw.join("");
106
+ expect(sql).toContain("node_chunks");
107
+ expect(sql).toContain("ORDER BY chunk_index ASC");
108
+ });
109
+ });
110
+ });
111
+ //# sourceMappingURL=chunk-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-service.test.js","sourceRoot":"","sources":["../../../src/services/__tests__/chunk-service.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chunker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.test.d.ts","sourceRoot":"","sources":["../../../src/services/__tests__/chunker.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { chunkText, estimateTokens, MAX_CHUNK_TOKENS, } from "../chunker.js";
3
+ describe("estimateTokens", () => {
4
+ it("estimates ~4 chars per token", () => {
5
+ expect(estimateTokens("abcd")).toBe(1);
6
+ expect(estimateTokens("abcdefgh")).toBe(2);
7
+ expect(estimateTokens("a")).toBe(1); // ceil(1/4) = 1
8
+ });
9
+ });
10
+ describe("chunkText", () => {
11
+ it("returns empty array for empty content", () => {
12
+ expect(chunkText("")).toEqual([]);
13
+ expect(chunkText(" ")).toEqual([]);
14
+ });
15
+ it("returns single chunk for short text", () => {
16
+ const text = "This is a short paragraph.";
17
+ const chunks = chunkText(text);
18
+ expect(chunks).toHaveLength(1);
19
+ expect(chunks[0].content).toBe(text);
20
+ expect(chunks[0].chunkIndex).toBe(0);
21
+ expect(chunks[0].overlapPrev).toBe(0);
22
+ expect(chunks[0].tokenCount).toBeGreaterThan(0);
23
+ });
24
+ it("splits long text at paragraph boundaries", () => {
25
+ // Create text with multiple paragraphs that exceed TARGET_CHUNK_TOKENS
26
+ const para = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(20);
27
+ const text = `${para}\n\n${para}\n\n${para}`;
28
+ const chunks = chunkText(text);
29
+ expect(chunks.length).toBeGreaterThan(1);
30
+ // Each chunk should be within bounds
31
+ for (const chunk of chunks) {
32
+ expect(chunk.tokenCount).toBeLessThanOrEqual(MAX_CHUNK_TOKENS);
33
+ }
34
+ });
35
+ it("carries heading context into chunks", () => {
36
+ const text = `# Introduction
37
+
38
+ First paragraph under intro.
39
+
40
+ # Details
41
+
42
+ Detail paragraph here with some content.`;
43
+ const chunks = chunkText(text);
44
+ expect(chunks.length).toBeGreaterThanOrEqual(2);
45
+ const introChunk = chunks.find((c) => c.content.includes("First paragraph"));
46
+ expect(introChunk?.headingContext).toBe("Introduction");
47
+ const detailChunk = chunks.find((c) => c.content.includes("Detail paragraph"));
48
+ expect(detailChunk?.headingContext).toBe("Details");
49
+ });
50
+ it("does not split code blocks", () => {
51
+ const codeBlock = "```javascript\n" + "const x = 1;\n".repeat(50) + "```";
52
+ const text = `Some intro text.\n\n${codeBlock}\n\nSome outro text.`;
53
+ const chunks = chunkText(text);
54
+ // Find the chunk containing the code block
55
+ const codeChunk = chunks.find((c) => c.content.includes("```javascript"));
56
+ expect(codeChunk).toBeDefined();
57
+ expect(codeChunk.content).toContain("const x = 1;");
58
+ expect(codeChunk.content).toContain("```");
59
+ });
60
+ it("has overlap = 0 at structural boundaries", () => {
61
+ // Multiple short paragraphs that fit in separate chunks due to accumulation
62
+ const para = "Short paragraph. ".repeat(40); // ~160 tokens each
63
+ const text = `${para}\n\n${para}\n\n${para}`;
64
+ const chunks = chunkText(text);
65
+ // Structural boundaries (paragraph breaks) should have overlap = 0
66
+ for (const chunk of chunks) {
67
+ if (chunk.chunkIndex === 0 || chunk.content.startsWith("Short paragraph")) {
68
+ expect(chunk.overlapPrev).toBe(0);
69
+ }
70
+ }
71
+ });
72
+ it("produces multiple chunks for forced mid-paragraph splits of long text", () => {
73
+ // Single huge paragraph that forces mid-paragraph splitting
74
+ const longPara = "This is a sentence that keeps going. ".repeat(100);
75
+ const chunks = chunkText(longPara);
76
+ expect(chunks.length).toBeGreaterThan(1);
77
+ // Each chunk should respect the max token limit
78
+ for (const chunk of chunks) {
79
+ expect(chunk.tokenCount).toBeLessThanOrEqual(MAX_CHUNK_TOKENS);
80
+ }
81
+ });
82
+ it("produces correct offsets", () => {
83
+ const text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.";
84
+ const chunks = chunkText(text);
85
+ // All chunks should have valid offset ranges
86
+ for (const chunk of chunks) {
87
+ expect(chunk.startOffset).toBeGreaterThanOrEqual(0);
88
+ expect(chunk.endOffset).toBeGreaterThan(chunk.startOffset);
89
+ }
90
+ });
91
+ it("assigns sequential chunk indices", () => {
92
+ const para = "Content paragraph with some text. ".repeat(30);
93
+ const text = `# Section 1\n\n${para}\n\n# Section 2\n\n${para}`;
94
+ const chunks = chunkText(text);
95
+ for (let i = 0; i < chunks.length; i++) {
96
+ expect(chunks[i].chunkIndex).toBe(i);
97
+ }
98
+ });
99
+ it("handles text with no headings", () => {
100
+ const text = "Just plain text without any markdown headings.\n\nAnother paragraph here.";
101
+ const chunks = chunkText(text);
102
+ expect(chunks.length).toBeGreaterThanOrEqual(1);
103
+ expect(chunks[0].headingContext).toBeNull();
104
+ });
105
+ it("handles text with only headings", () => {
106
+ const text = "# Heading 1\n\n## Heading 2";
107
+ const chunks = chunkText(text);
108
+ // Headings are parsed as structure, remaining text is minimal
109
+ // The exact count depends on how headings are parsed, but should be small
110
+ expect(chunks.length).toBeLessThanOrEqual(1);
111
+ });
112
+ });
113
+ //# sourceMappingURL=chunker.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunker.test.js","sourceRoot":"","sources":["../../../src/services/__tests__/chunker.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=node-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-service.test.d.ts","sourceRoot":"","sources":["../../../src/services/__tests__/node-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { NodeService } from "../node-service.js";
3
+ import { PiiDetectorService } from "../pii-detector-service.js";
4
+ import { PiiEncryptionService } from "../pii-encryption-service.js";
5
+ function createMockDb() {
6
+ const rawCalls = [];
7
+ return {
8
+ node: {
9
+ create: vi.fn().mockResolvedValue({
10
+ id: "00000000-0000-0000-0000-000000000001",
11
+ type: "note",
12
+ title: "test",
13
+ content: "test",
14
+ metadata: {},
15
+ status: "draft",
16
+ createdBy: "test",
17
+ createdAt: new Date(),
18
+ updatedAt: new Date(),
19
+ version: 1,
20
+ hasPii: false,
21
+ domainId: null,
22
+ organizationId: "org-1",
23
+ }),
24
+ findUnique: vi.fn().mockResolvedValue(null),
25
+ update: vi.fn().mockResolvedValue({}),
26
+ },
27
+ $executeRaw: vi.fn((...args) => {
28
+ rawCalls.push(args);
29
+ return Promise.resolve(1);
30
+ }),
31
+ $queryRaw: vi.fn().mockResolvedValue([]),
32
+ _rawCalls: rawCalls,
33
+ };
34
+ }
35
+ function createMockEmbeddings() {
36
+ return {
37
+ lastInput: null,
38
+ embed: vi.fn(async function (text) {
39
+ this.lastInput = text;
40
+ return [0.1, 0.2, 0.3];
41
+ }),
42
+ embedBatch: vi.fn(async (texts) => texts.map(() => [0.1, 0.2, 0.3])),
43
+ };
44
+ }
45
+ describe("NodeService", () => {
46
+ const detector = new PiiDetectorService();
47
+ const piiEncryption = new PiiEncryptionService(detector);
48
+ const tenantCtx = { organizationId: "org-1" };
49
+ describe("create — PII stripping for embeddings", () => {
50
+ let db;
51
+ let embeddings;
52
+ beforeEach(() => {
53
+ db = createMockDb();
54
+ embeddings = createMockEmbeddings();
55
+ });
56
+ it("strips email before embed() in encrypt mode", async () => {
57
+ const service = new NodeService(db, embeddings, tenantCtx, piiEncryption, {
58
+ mode: "encrypt",
59
+ orgKey: Buffer.alloc(32),
60
+ });
61
+ await service.create({
62
+ type: "note",
63
+ title: "Contact",
64
+ content: "Email john@example.com for info",
65
+ createdBy: "test",
66
+ });
67
+ // embed() should receive stripped text
68
+ expect(embeddings.embed).toHaveBeenCalledWith(expect.stringContaining("[EMAIL]"));
69
+ expect(embeddings.embed).toHaveBeenCalledWith(expect.not.stringContaining("john@example.com"));
70
+ });
71
+ it("does not strip when mode is disabled", async () => {
72
+ const service = new NodeService(db, embeddings, tenantCtx, piiEncryption, {
73
+ mode: "disabled",
74
+ });
75
+ await service.create({
76
+ type: "note",
77
+ title: "Contact",
78
+ content: "Email john@example.com for info",
79
+ createdBy: "test",
80
+ });
81
+ expect(embeddings.embed).toHaveBeenCalledWith(expect.stringContaining("john@example.com"));
82
+ });
83
+ it("strips PII before embed() in detect-warn mode", async () => {
84
+ const service = new NodeService(db, embeddings, tenantCtx, piiEncryption, {
85
+ mode: "detect-warn",
86
+ });
87
+ await service.create({
88
+ type: "note",
89
+ title: "Contact",
90
+ content: "Email john@example.com for info",
91
+ createdBy: "test",
92
+ });
93
+ // In detect-warn, embedding text should still be stripped
94
+ expect(embeddings.embed).toHaveBeenCalledWith(expect.stringContaining("[EMAIL]"));
95
+ });
96
+ it("calls chunkService.chunkAndEmbed when provided", async () => {
97
+ const chunkService = {
98
+ chunkAndEmbed: vi.fn().mockResolvedValue(undefined),
99
+ deleteByNodeId: vi.fn().mockResolvedValue(undefined),
100
+ getByNodeId: vi.fn().mockResolvedValue([]),
101
+ };
102
+ const service = new NodeService(db, embeddings, tenantCtx, undefined, undefined, chunkService);
103
+ await service.create({
104
+ type: "note",
105
+ title: "Test",
106
+ content: "Some content",
107
+ createdBy: "test",
108
+ });
109
+ expect(chunkService.chunkAndEmbed).toHaveBeenCalledWith("00000000-0000-0000-0000-000000000001", "Test", "Some content");
110
+ });
111
+ it("works without chunkService (backward compat)", async () => {
112
+ const service = new NodeService(db, embeddings, tenantCtx);
113
+ const result = await service.create({
114
+ type: "note",
115
+ title: "Test",
116
+ content: "Some content",
117
+ createdBy: "test",
118
+ });
119
+ expect(result).toBeDefined();
120
+ expect(result.id).toBe("00000000-0000-0000-0000-000000000001");
121
+ });
122
+ it("stamps embedding_pii_mode in raw SQL", async () => {
123
+ const service = new NodeService(db, embeddings, tenantCtx, piiEncryption, {
124
+ mode: "encrypt",
125
+ orgKey: Buffer.alloc(32),
126
+ });
127
+ await service.create({
128
+ type: "note",
129
+ title: "test",
130
+ content: "test content",
131
+ createdBy: "test",
132
+ });
133
+ // The $executeRaw should have been called with the pii mode
134
+ expect(db.$executeRaw).toHaveBeenCalled();
135
+ // Check that the raw SQL template includes embedding_pii_mode
136
+ const call = db.$executeRaw.mock.calls[0];
137
+ // Tagged template: first arg is TemplateStringsArray
138
+ const templateStrings = call[0];
139
+ const sqlParts = templateStrings.raw.join("");
140
+ expect(sqlParts).toContain("embedding_pii_mode");
141
+ });
142
+ });
143
+ describe("update — re-chunks when content changes", () => {
144
+ let db;
145
+ let embeddings;
146
+ beforeEach(() => {
147
+ db = createMockDb();
148
+ embeddings = createMockEmbeddings();
149
+ // Mock findUnique to return an existing node (needed by update)
150
+ db.node.findUnique = vi.fn().mockResolvedValue({
151
+ id: "00000000-0000-0000-0000-000000000001",
152
+ type: "note",
153
+ title: "Old Title",
154
+ content: "Old Content",
155
+ metadata: {},
156
+ status: "draft",
157
+ createdBy: "test",
158
+ createdAt: new Date(),
159
+ updatedAt: new Date(),
160
+ version: 1,
161
+ hasPii: false,
162
+ domainId: null,
163
+ organizationId: "org-1",
164
+ });
165
+ db.node.update = vi.fn().mockResolvedValue({
166
+ id: "00000000-0000-0000-0000-000000000001",
167
+ type: "note",
168
+ title: "New Title",
169
+ content: "New Content",
170
+ metadata: {},
171
+ status: "draft",
172
+ createdBy: "test",
173
+ createdAt: new Date(),
174
+ updatedAt: new Date(),
175
+ version: 2,
176
+ hasPii: false,
177
+ domainId: null,
178
+ organizationId: "org-1",
179
+ });
180
+ });
181
+ it("re-chunks when content changes", async () => {
182
+ const chunkService = {
183
+ chunkAndEmbed: vi.fn().mockResolvedValue(undefined),
184
+ deleteByNodeId: vi.fn().mockResolvedValue(undefined),
185
+ getByNodeId: vi.fn().mockResolvedValue([]),
186
+ };
187
+ const service = new NodeService(db, embeddings, tenantCtx, undefined, undefined, chunkService);
188
+ await service.update("00000000-0000-0000-0000-000000000001", {
189
+ content: "New Content",
190
+ });
191
+ expect(chunkService.chunkAndEmbed).toHaveBeenCalledWith("00000000-0000-0000-0000-000000000001", "Old Title", "New Content");
192
+ });
193
+ it("does not re-chunk when only metadata changes", async () => {
194
+ const chunkService = {
195
+ chunkAndEmbed: vi.fn().mockResolvedValue(undefined),
196
+ deleteByNodeId: vi.fn().mockResolvedValue(undefined),
197
+ getByNodeId: vi.fn().mockResolvedValue([]),
198
+ };
199
+ const service = new NodeService(db, embeddings, tenantCtx, undefined, undefined, chunkService);
200
+ await service.update("00000000-0000-0000-0000-000000000001", {
201
+ metadata: { foo: "bar" },
202
+ });
203
+ expect(chunkService.chunkAndEmbed).not.toHaveBeenCalled();
204
+ });
205
+ });
206
+ });
207
+ //# sourceMappingURL=node-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-service.test.js","sourceRoot":"","sources":["../../../src/services/__tests__/node-service.test.ts"],"names":[],"mappings":"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"}
@@ -50,6 +50,57 @@ describe("PiiDetectorService", () => {
50
50
  expect(text.slice(match.start, match.end)).toBe("john@example.com");
51
51
  });
52
52
  });
53
+ describe("Norwegian patterns", () => {
54
+ // Synthetic valid fødselsnummer: 01010150074
55
+ // DD=01 MM=01 YY=01 IND=500 K1=2 K2=2 — passes modulo-11
56
+ it("detects valid fødselsnummer", () => {
57
+ const result = detector.detect("FNR: 01010150074");
58
+ expect(result.hasPii).toBe(true);
59
+ expect(result.matches.some((m) => m.type === "fodselsnummer")).toBe(true);
60
+ expect(result.matches.find((m) => m.type === "fodselsnummer").value).toBe("01010150074");
61
+ });
62
+ // D-nummer: first digit +40, so DD=41 → day 1
63
+ it("detects valid D-nummer", () => {
64
+ const result = detector.detect("D-nr: 41019004927");
65
+ expect(result.hasPii).toBe(true);
66
+ expect(result.matches.some((m) => m.type === "fodselsnummer")).toBe(true);
67
+ });
68
+ it("rejects invalid 11-digit numbers (bad checksum)", () => {
69
+ const result = detector.detect("Invalid: 12345678901");
70
+ // Should not match as fodselsnummer
71
+ const fnrMatches = result.matches.filter((m) => m.type === "fodselsnummer");
72
+ expect(fnrMatches).toHaveLength(0);
73
+ });
74
+ it("detects valid organisasjonsnummer", () => {
75
+ // 912345678: starts with 9, checksum: 3*9+2*1+7*2+6*3+5*4+4*5+3*6+2*7 = 27+2+14+18+20+20+18+14 = 133
76
+ // 133 % 11 = 1, check = 11-1 = 10 → invalid
77
+ // Use a synthetic valid one: 987654325
78
+ // weights [3,2,7,6,5,4,3,2]: 3*9+2*8+7*7+6*6+5*5+4*4+3*3+2*2 = 27+16+49+36+25+16+9+4 = 182
79
+ // 182 % 11 = 6, check = 11-6 = 5 → digit 9 should be 5 ✓
80
+ const result = detector.detect("Org: 987654325");
81
+ expect(result.hasPii).toBe(true);
82
+ expect(result.matches.some((m) => m.type === "organisasjonsnummer")).toBe(true);
83
+ expect(result.matches.find((m) => m.type === "organisasjonsnummer").value).toBe("987654325");
84
+ });
85
+ it("rejects invalid 9-digit numbers (bad checksum)", () => {
86
+ const result = detector.detect("Org: 987654321");
87
+ const orgMatches = result.matches.filter((m) => m.type === "organisasjonsnummer");
88
+ expect(orgMatches).toHaveLength(0);
89
+ });
90
+ it("rejects 9-digit numbers not starting with 8 or 9", () => {
91
+ const result = detector.detect("Number: 123456789");
92
+ const orgMatches = result.matches.filter((m) => m.type === "organisasjonsnummer");
93
+ expect(orgMatches).toHaveLength(0);
94
+ });
95
+ it("coexists with existing email and SSN patterns", () => {
96
+ const result = detector.detect("Email: john@example.com, SSN: 123-45-6789, FNR: 01010150074");
97
+ expect(result.hasPii).toBe(true);
98
+ const types = result.matches.map((m) => m.type);
99
+ expect(types).toContain("email");
100
+ expect(types).toContain("ssn_us");
101
+ expect(types).toContain("fodselsnummer");
102
+ });
103
+ });
53
104
  describe("detectInObject", () => {
54
105
  it("detects PII in string values", () => {
55
106
  const results = detector.detectInObject({
@@ -1 +1 @@
1
- {"version":3,"file":"pii-detector-service.test.js","sourceRoot":"","sources":["../../../src/services/__tests__/pii-detector-service.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pii-detector-service.test.js","sourceRoot":"","sources":["../../../src/services/__tests__/pii-detector-service.test.ts"],"names":[],"mappings":"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"}
@@ -133,6 +133,43 @@ describe("PiiEncryptionService", () => {
133
133
  expect(decrypted.content).toBe(original.content);
134
134
  });
135
135
  });
136
+ describe("stripPii", () => {
137
+ it("strips email → [EMAIL]", () => {
138
+ const result = service.stripPii("Contact john@example.com please");
139
+ expect(result.text).toBe("Contact [EMAIL] please");
140
+ expect(result.piiDetected).toBe(true);
141
+ expect(result.types).toContain("email");
142
+ });
143
+ it("strips fødselsnummer → [FODSELSNUMMER]", () => {
144
+ const result = service.stripPii("FNR: 01010150074");
145
+ expect(result.text).toBe("FNR: [FODSELSNUMMER]");
146
+ expect(result.piiDetected).toBe(true);
147
+ expect(result.types).toContain("fodselsnummer");
148
+ });
149
+ it("respects allowedTypes — listed types pass through", () => {
150
+ const result = service.stripPii("Email john@example.com, SSN 123-45-6789", ["email"]);
151
+ expect(result.text).toContain("john@example.com");
152
+ expect(result.text).not.toContain("123-45-6789");
153
+ expect(result.text).toContain("[SSN_US]");
154
+ });
155
+ it("strips multiple PII in same text", () => {
156
+ const result = service.stripPii("Email john@example.com SSN 123-45-6789");
157
+ expect(result.text).not.toContain("john@example.com");
158
+ expect(result.text).not.toContain("123-45-6789");
159
+ expect(result.text).toContain("[EMAIL]");
160
+ expect(result.text).toContain("[SSN_US]");
161
+ });
162
+ it("returns unchanged text when no PII", () => {
163
+ const result = service.stripPii("Just a normal sentence");
164
+ expect(result.text).toBe("Just a normal sentence");
165
+ expect(result.piiDetected).toBe(false);
166
+ });
167
+ it("returns unchanged for empty text", () => {
168
+ const result = service.stripPii("");
169
+ expect(result.text).toBe("");
170
+ expect(result.piiDetected).toBe(false);
171
+ });
172
+ });
136
173
  describe("token format", () => {
137
174
  it("produces parseable PII tokens", () => {
138
175
  const result = service.processText("Email john@example.com", {