@unrdf/kgc-probe 26.4.2

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.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Tests for Filesystem Probe
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { probeFilesystem, guardPath } from './filesystem.mjs';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ describe('Filesystem Probe', () => {
12
+ let testRoot;
13
+ let outDir;
14
+
15
+ beforeEach(async () => {
16
+ // Create temp test directory
17
+ testRoot = path.join(os.tmpdir(), `kgc-probe-test-${Date.now()}`);
18
+ outDir = path.join(testRoot, 'out');
19
+ await fs.mkdir(outDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(async () => {
23
+ // Cleanup
24
+ try {
25
+ await fs.rm(testRoot, { recursive: true, force: true });
26
+ } catch {}
27
+ });
28
+
29
+ describe('guardPath', () => {
30
+ it('should allow paths within allowed roots', () => {
31
+ const result = guardPath('/home/user/project/file.txt', ['/home/user/project']);
32
+ expect(result.allowed).toBe(true);
33
+ });
34
+
35
+ it('should deny paths outside allowed roots', () => {
36
+ const result = guardPath('/etc/passwd', ['/home/user/project']);
37
+ expect(result.allowed).toBe(false);
38
+ expect(result.reason).toContain('outside allowed roots');
39
+ });
40
+
41
+ it('should deny /etc/ paths', () => {
42
+ const result = guardPath('/etc/hosts', ['/etc']);
43
+ expect(result.allowed).toBe(false);
44
+ expect(result.reason).toContain('forbidden pattern');
45
+ });
46
+
47
+ it('should deny /root/ paths', () => {
48
+ const result = guardPath('/root/secret.txt', ['/root']);
49
+ expect(result.allowed).toBe(false);
50
+ expect(result.reason).toContain('forbidden pattern');
51
+ });
52
+
53
+ it('should deny .ssh directories', () => {
54
+ const result = guardPath('/home/user/.ssh/id_rsa', ['/home/user']);
55
+ expect(result.allowed).toBe(false);
56
+ expect(result.reason).toContain('forbidden pattern');
57
+ });
58
+
59
+ it('should deny .env files', () => {
60
+ const result = guardPath('/home/user/project/.env', ['/home/user/project']);
61
+ expect(result.allowed).toBe(false);
62
+ expect(result.reason).toContain('forbidden pattern');
63
+ });
64
+
65
+ it('should deny credentials.json', () => {
66
+ const result = guardPath('/home/user/credentials.json', ['/home/user']);
67
+ expect(result.allowed).toBe(false);
68
+ expect(result.reason).toContain('forbidden pattern');
69
+ });
70
+
71
+ it('should deny .pem files', () => {
72
+ const result = guardPath('/home/user/cert.pem', ['/home/user']);
73
+ expect(result.allowed).toBe(false);
74
+ expect(result.reason).toContain('forbidden pattern');
75
+ });
76
+ });
77
+
78
+ describe('probeFilesystem', () => {
79
+ it('should return observations array', async () => {
80
+ const observations = await probeFilesystem({
81
+ roots: [testRoot],
82
+ out: outDir,
83
+ budgetMs: 5000
84
+ });
85
+
86
+ expect(Array.isArray(observations)).toBe(true);
87
+ expect(observations.length).toBeGreaterThan(0);
88
+ });
89
+
90
+ it('should have valid observation structure', async () => {
91
+ const observations = await probeFilesystem({
92
+ roots: [testRoot],
93
+ out: outDir,
94
+ budgetMs: 5000
95
+ });
96
+
97
+ const obs = observations[0];
98
+ expect(obs).toHaveProperty('method');
99
+ expect(obs).toHaveProperty('inputs');
100
+ expect(obs).toHaveProperty('timestamp');
101
+ expect(obs).toHaveProperty('hash');
102
+ expect(obs).toHaveProperty('guardDecision');
103
+ });
104
+
105
+ it('should deny access to forbidden paths', async () => {
106
+ const observations = await probeFilesystem({
107
+ roots: ['/etc'],
108
+ out: '/etc/kgc-probe',
109
+ budgetMs: 5000
110
+ });
111
+
112
+ const deniedObs = observations.find(o => o.guardDecision === 'denied');
113
+ expect(deniedObs).toBeDefined();
114
+ expect(deniedObs.guardReason).toContain('forbidden pattern');
115
+ });
116
+
117
+ it('should deny when output directory outside roots', async () => {
118
+ const observations = await probeFilesystem({
119
+ roots: [testRoot],
120
+ out: '/tmp/outside-roots',
121
+ budgetMs: 5000
122
+ });
123
+
124
+ const mainObs = observations.find(o => o.method === 'probeFilesystem');
125
+ if (mainObs) {
126
+ expect(mainObs.guardDecision).toBe('denied');
127
+ }
128
+ });
129
+
130
+ it('should not include outputs for denied operations', async () => {
131
+ const observations = await probeFilesystem({
132
+ roots: ['/etc'],
133
+ out: '/etc/test',
134
+ budgetMs: 5000
135
+ });
136
+
137
+ const deniedObs = observations.find(o => o.guardDecision === 'denied');
138
+ if (deniedObs) {
139
+ expect(deniedObs.outputs).toBeUndefined();
140
+ }
141
+ });
142
+
143
+ it('should complete within budget', async () => {
144
+ const start = Date.now();
145
+ await probeFilesystem({
146
+ roots: [testRoot],
147
+ out: outDir,
148
+ budgetMs: 3000
149
+ });
150
+ const elapsed = Date.now() - start;
151
+
152
+ expect(elapsed).toBeLessThan(4000); // Allow some margin
153
+ });
154
+
155
+ it('should probe read capability', async () => {
156
+ const observations = await probeFilesystem({
157
+ roots: [testRoot],
158
+ out: outDir,
159
+ budgetMs: 5000
160
+ });
161
+
162
+ const readObs = observations.find(o => o.method === 'fs.access(R_OK)');
163
+ expect(readObs).toBeDefined();
164
+ expect(readObs.guardDecision).toBe('allowed');
165
+ });
166
+
167
+ it('should probe write capability', async () => {
168
+ const observations = await probeFilesystem({
169
+ roots: [testRoot],
170
+ out: outDir,
171
+ budgetMs: 5000
172
+ });
173
+
174
+ const writeObs = observations.find(o => o.method === 'fs.access(W_OK)');
175
+ expect(writeObs).toBeDefined();
176
+ expect(writeObs.guardDecision).toBe('allowed');
177
+ });
178
+
179
+ it('should probe symlink behavior', async () => {
180
+ const observations = await probeFilesystem({
181
+ roots: [testRoot],
182
+ out: outDir,
183
+ budgetMs: 5000
184
+ });
185
+
186
+ const symlinkObs = observations.find(o => o.method === 'fs.symlink');
187
+ expect(symlinkObs).toBeDefined();
188
+ expect(symlinkObs.guardDecision).toBe('allowed');
189
+ });
190
+
191
+ it('should probe directory traversal', async () => {
192
+ const observations = await probeFilesystem({
193
+ roots: [testRoot],
194
+ out: outDir,
195
+ budgetMs: 5000
196
+ });
197
+
198
+ const traversalObs = observations.find(o => o.method === 'fs.readdir(recursive)');
199
+ expect(traversalObs).toBeDefined();
200
+ expect(traversalObs.guardDecision).toBe('allowed');
201
+ });
202
+
203
+ it('should have deterministic hashes', async () => {
204
+ const obs1 = await probeFilesystem({
205
+ roots: [testRoot],
206
+ out: outDir,
207
+ budgetMs: 5000
208
+ });
209
+
210
+ const obs2 = await probeFilesystem({
211
+ roots: [testRoot],
212
+ out: outDir,
213
+ budgetMs: 5000
214
+ });
215
+
216
+ // Methods should be same
217
+ expect(obs1.map(o => o.method).sort()).toEqual(obs2.map(o => o.method).sort());
218
+ });
219
+ });
220
+
221
+ describe('Observation Schema Compliance', () => {
222
+ it('should include all required fields', async () => {
223
+ const observations = await probeFilesystem({
224
+ roots: [testRoot],
225
+ out: outDir,
226
+ budgetMs: 5000
227
+ });
228
+
229
+ for (const obs of observations) {
230
+ expect(obs.method).toBeDefined();
231
+ expect(obs.inputs).toBeDefined();
232
+ expect(obs.timestamp).toBeDefined();
233
+ expect(obs.hash).toBeDefined();
234
+ expect(obs.guardDecision).toBeDefined();
235
+
236
+ // Validate timestamp format
237
+ expect(() => new Date(obs.timestamp)).not.toThrow();
238
+
239
+ // Validate hash format (SHA256 = 64 hex chars)
240
+ expect(obs.hash).toMatch(/^[a-f0-9]{64}$/);
241
+ }
242
+ });
243
+ });
244
+ });