@openparachute/vault 0.7.9-rc.3 → 0.7.9-rc.4
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.
- package/core/src/link-count.test.ts +29 -0
- package/core/src/links.ts +43 -0
- package/core/src/mcp-manifest.ts +6 -6
- package/core/src/mcp.ts +21 -11
- package/core/src/wikilinks.ts +68 -1
- package/package.json +1 -1
- package/src/release-plan.test.ts +90 -1
- package/src/routes.ts +69 -9
- package/src/test-support/vault-714-find-path.json +58 -0
- package/src/test-support/vault-714-graph.json +27 -0
- package/src/test-support/vault-714-has_links.json +96 -0
- package/src/test-support/vault-714-include_broken_links.json +118 -0
- package/src/test-support/vault-714-include_link_count.json +348 -0
- package/src/test-support/vault-714-include_links.json +86 -0
- package/src/test-support/vault-714-near.json +78 -0
- package/src/test-support/vault-714-unresolved-wikilinks.json +6 -0
- package/src/vault.test.ts +243 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"MCP": {
|
|
3
|
+
"id": "work-src",
|
|
4
|
+
"content": "src [[Dup]]",
|
|
5
|
+
"path": "work-src",
|
|
6
|
+
"extension": "md",
|
|
7
|
+
"metadata": {},
|
|
8
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
9
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
10
|
+
"createdBy": null,
|
|
11
|
+
"createdVia": null,
|
|
12
|
+
"lastUpdatedBy": null,
|
|
13
|
+
"lastUpdatedVia": null,
|
|
14
|
+
"tags": [
|
|
15
|
+
"work"
|
|
16
|
+
],
|
|
17
|
+
"links": [
|
|
18
|
+
{
|
|
19
|
+
"sourceId": "work-src",
|
|
20
|
+
"targetId": "hidden-dup",
|
|
21
|
+
"relationship": "wikilink",
|
|
22
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
23
|
+
"sourceNote": {
|
|
24
|
+
"id": "work-src",
|
|
25
|
+
"path": "work-src",
|
|
26
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
27
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
28
|
+
"tags": [
|
|
29
|
+
"work"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"targetNote": {
|
|
33
|
+
"id": "hidden-dup",
|
|
34
|
+
"path": "p1/Dup",
|
|
35
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
36
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
37
|
+
"tags": [
|
|
38
|
+
"personal"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"REST": {
|
|
45
|
+
"id": "work-src",
|
|
46
|
+
"content": "src [[Dup]]",
|
|
47
|
+
"path": "work-src",
|
|
48
|
+
"extension": "md",
|
|
49
|
+
"metadata": {},
|
|
50
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
51
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
52
|
+
"createdBy": null,
|
|
53
|
+
"createdVia": null,
|
|
54
|
+
"lastUpdatedBy": null,
|
|
55
|
+
"lastUpdatedVia": null,
|
|
56
|
+
"tags": [
|
|
57
|
+
"work"
|
|
58
|
+
],
|
|
59
|
+
"links": [
|
|
60
|
+
{
|
|
61
|
+
"sourceId": "work-src",
|
|
62
|
+
"targetId": "hidden-dup",
|
|
63
|
+
"relationship": "wikilink",
|
|
64
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
65
|
+
"sourceNote": {
|
|
66
|
+
"id": "work-src",
|
|
67
|
+
"path": "work-src",
|
|
68
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
69
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
70
|
+
"tags": [
|
|
71
|
+
"work"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"targetNote": {
|
|
75
|
+
"id": "hidden-dup",
|
|
76
|
+
"path": "p1/Dup",
|
|
77
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
78
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
79
|
+
"tags": [
|
|
80
|
+
"personal"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"MCP": [
|
|
3
|
+
{
|
|
4
|
+
"id": "hidden-dup",
|
|
5
|
+
"path": "p1/Dup",
|
|
6
|
+
"extension": "md",
|
|
7
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
8
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
9
|
+
"createdBy": null,
|
|
10
|
+
"createdVia": null,
|
|
11
|
+
"lastUpdatedBy": null,
|
|
12
|
+
"lastUpdatedVia": null,
|
|
13
|
+
"tags": [
|
|
14
|
+
"personal"
|
|
15
|
+
],
|
|
16
|
+
"metadata": {},
|
|
17
|
+
"byteSize": 12,
|
|
18
|
+
"preview": "personal dup",
|
|
19
|
+
"displayTitle": "personal dup"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "work-src",
|
|
23
|
+
"path": "work-src",
|
|
24
|
+
"extension": "md",
|
|
25
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
26
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
27
|
+
"createdBy": null,
|
|
28
|
+
"createdVia": null,
|
|
29
|
+
"lastUpdatedBy": null,
|
|
30
|
+
"lastUpdatedVia": null,
|
|
31
|
+
"tags": [
|
|
32
|
+
"work"
|
|
33
|
+
],
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"byteSize": 11,
|
|
36
|
+
"preview": "src [[Dup]]",
|
|
37
|
+
"displayTitle": "src [[Dup]]"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"REST": [
|
|
41
|
+
{
|
|
42
|
+
"id": "hidden-dup",
|
|
43
|
+
"path": "p1/Dup",
|
|
44
|
+
"extension": "md",
|
|
45
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
46
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
47
|
+
"createdBy": null,
|
|
48
|
+
"createdVia": null,
|
|
49
|
+
"lastUpdatedBy": null,
|
|
50
|
+
"lastUpdatedVia": null,
|
|
51
|
+
"tags": [
|
|
52
|
+
"personal"
|
|
53
|
+
],
|
|
54
|
+
"metadata": {},
|
|
55
|
+
"byteSize": 12,
|
|
56
|
+
"preview": "personal dup",
|
|
57
|
+
"displayTitle": "personal dup"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "work-src",
|
|
61
|
+
"path": "work-src",
|
|
62
|
+
"extension": "md",
|
|
63
|
+
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
64
|
+
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
65
|
+
"createdBy": null,
|
|
66
|
+
"createdVia": null,
|
|
67
|
+
"lastUpdatedBy": null,
|
|
68
|
+
"lastUpdatedVia": null,
|
|
69
|
+
"tags": [
|
|
70
|
+
"work"
|
|
71
|
+
],
|
|
72
|
+
"metadata": {},
|
|
73
|
+
"byteSize": 11,
|
|
74
|
+
"preview": "src [[Dup]]",
|
|
75
|
+
"displayTitle": "src [[Dup]]"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
package/src/vault.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
6
|
import { Database } from "bun:sqlite";
|
|
7
|
-
import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
|
|
7
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync, readFileSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
10
|
import { BunStore } from "./vault-store.ts";
|
|
@@ -35,6 +35,138 @@ afterEach(() => {
|
|
|
35
35
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
|
|
39
|
+
// vault#714: one deterministic fixture through the real scoped MCP wrapper and
|
|
40
|
+
// REST handlers. The unscoped JSON fixture was captured on 20d0f6f before fixes.
|
|
41
|
+
async function edgeSuppressionFixture(door: "MCP" | "REST", name: string, scoped = true, shape = "F1", semantic = false) {
|
|
42
|
+
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
43
|
+
const { writeVaultConfig } = await import("./config.ts");
|
|
44
|
+
const { getVaultStore, closeAllStores } = await import("./vault-store.ts");
|
|
45
|
+
const vaultName = `tagscope-edge-${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
46
|
+
let fixtureStore = store;
|
|
47
|
+
if (door === "MCP") {
|
|
48
|
+
writeVaultConfig({ name: vaultName, api_keys: [], created_at: "2026-01-01T00:00:00.000Z" });
|
|
49
|
+
fixtureStore = getVaultStore(vaultName);
|
|
50
|
+
}
|
|
51
|
+
const note = (id: string, path: string, content: string, tag: string) =>
|
|
52
|
+
fixtureStore.createNote(content, { id, path, tags: [tag], created_at: "2026-01-01T00:00:00.000Z" });
|
|
53
|
+
const hidden = await note("hidden-dup", "p1/Dup", "personal dup", "personal");
|
|
54
|
+
if (shape === "F1") {
|
|
55
|
+
const competing = await note("visible-dup", "wdup/Dup", "work dup", "work");
|
|
56
|
+
await note("work-src", "work-src", "src [[Dup]]", "work");
|
|
57
|
+
await fixtureStore.deleteNote(competing.id);
|
|
58
|
+
} else {
|
|
59
|
+
await note("work-src", "work-src", "src", "work");
|
|
60
|
+
if (shape === "structured") await fixtureStore.createLink("work-src", hidden.id, "references");
|
|
61
|
+
if (shape === "F2") {
|
|
62
|
+
await note("work-tgt", "work-tgt", "target", "work");
|
|
63
|
+
await note("hidden-in", "p2/In", "see [[work-tgt]]", "personal");
|
|
64
|
+
}
|
|
65
|
+
if (shape === "F3") {
|
|
66
|
+
await note("work-tgt", "work-tgt", "target", "work");
|
|
67
|
+
await note("hidden-mid", "p3/Mid", "mid", "personal");
|
|
68
|
+
await fixtureStore.createLink("work-src", "hidden-mid", "references");
|
|
69
|
+
await fixtureStore.createLink("hidden-mid", "work-tgt", "references");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (semantic) {
|
|
73
|
+
// Exercise the real semantic engine with deterministic local vectors.
|
|
74
|
+
fixtureStore = new BunStore(fixtureStore.db, { embeddingProvider: {
|
|
75
|
+
name: "mock", model: "vault-714-test", dims: 4,
|
|
76
|
+
available: async () => ({ ok: true }),
|
|
77
|
+
embed: async (input) => ({ vectors: input.texts.map(() => new Float32Array([1, 0, 0, 0])), model: "vault-714-test", dims: 4 }),
|
|
78
|
+
} });
|
|
79
|
+
const { encodeVector } = await import("../core/src/embedding/vector-codec.ts");
|
|
80
|
+
for (const n of await fixtureStore.queryNotes({})) {
|
|
81
|
+
fixtureStore.db.prepare("INSERT INTO note_vectors (note_id, chunk_ix, vector, dims, model, content_hash, embedded_at) VALUES (?, 0, ?, 4, ?, ?, ?)")
|
|
82
|
+
.run(n.id, encodeVector(new Float32Array([1, 0, 0, 0])), "vault-714-test", "hash", "2026-01-01T00:00:00.000Z");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
fixtureStore.db.prepare("UPDATE notes SET updated_at = created_at").run();
|
|
86
|
+
fixtureStore.db.prepare("UPDATE links SET created_at = ?").run("2026-01-01T00:00:00.000Z");
|
|
87
|
+
const authForTags = (tags: string[]) => ({
|
|
88
|
+
scopes: ["vault:read", "vault:write", "vault:admin"], legacyDerived: false, scoped_tags: tags,
|
|
89
|
+
});
|
|
90
|
+
const tools = door === "MCP" ? generateScopedMcpTools(vaultName, scoped ? authForTags(["work"]) as any : undefined) : [];
|
|
91
|
+
const scope: TagScopeCtx = scoped
|
|
92
|
+
? { allowed: await expandTokenTagScope(fixtureStore, ["work"]), raw: ["work"] }
|
|
93
|
+
: { allowed: null, raw: null };
|
|
94
|
+
const restQuery = (params: Record<string, any>) => {
|
|
95
|
+
const search = new URLSearchParams();
|
|
96
|
+
for (const [key, value] of Object.entries(params)) {
|
|
97
|
+
if (typeof value === "object") {
|
|
98
|
+
for (const [k, v] of Object.entries(value)) search.set(`${key}[${k}]`, String(v));
|
|
99
|
+
} else search.set(key, String(value));
|
|
100
|
+
}
|
|
101
|
+
return handleNotes(mkReq("GET", `/notes?${search}`), fixtureStore, "", "v", scope);
|
|
102
|
+
};
|
|
103
|
+
const query = async (params: Record<string, any>): Promise<any> => door === "MCP"
|
|
104
|
+
? tools.find((t) => t.name === "query-notes")!.execute(params)
|
|
105
|
+
: (await restQuery(params)).json();
|
|
106
|
+
const find = async (target: string): Promise<any> => door === "MCP"
|
|
107
|
+
? tools.find((t) => t.name === "find-path")!.execute({ source: "work-src", target })
|
|
108
|
+
: (await handleFindPath(mkReq("GET", `/find-path?source=work-src&target=${encodeURIComponent(target)}`), fixtureStore, scope)).json();
|
|
109
|
+
const unresolved = async (): Promise<any> => handleUnresolvedWikilinks(mkReq("GET", "/unresolved-wikilinks"), fixtureStore, scope).json();
|
|
110
|
+
const leak = (resp: unknown) => {
|
|
111
|
+
const json = JSON.stringify(resp);
|
|
112
|
+
for (const secret of [hidden.id, "p1/Dup", "hidden-mid", "p3/Mid", "hidden-in", "p2/In"]) expect(json).not.toContain(secret);
|
|
113
|
+
};
|
|
114
|
+
return { door, query, find, unresolved, leak, hidden, store: fixtureStore, scope, close: closeAllStores };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type EdgeFixture = Awaited<ReturnType<typeof edgeSuppressionFixture>>;
|
|
118
|
+
const edgeScopedPins: { name: string; shape?: string; run: (f: EdgeFixture) => Promise<void> }[] = [
|
|
119
|
+
{ name: "both degree suppresses hidden outbound", run: async (f) => {
|
|
120
|
+
const r = await f.query({ id: "work-src", include_link_count: true }); f.leak(r); expect(r.linkCount).toBe(0);
|
|
121
|
+
const list = await f.query({ include_link_count: true }); f.leak(list); expect(list[0].linkCount).toBe(0);
|
|
122
|
+
} },
|
|
123
|
+
{ name: "outbound degree suppresses hidden target", run: async (f) => {
|
|
124
|
+
const r = await f.query({ id: "work-src", include_link_count: true, link_count_direction: "outbound" }); f.leak(r); expect(r.linkCount).toBe(0);
|
|
125
|
+
} },
|
|
126
|
+
{ name: "inbound degree suppresses hidden source", shape: "F2", run: async (f) => {
|
|
127
|
+
const r = await f.query({ id: "work-tgt", include_link_count: true, link_count_direction: "inbound" }); f.leak(r); expect(r.linkCount).toBe(0);
|
|
128
|
+
} },
|
|
129
|
+
{ name: "has_links true excludes hidden-only edge", run: async (f) => {
|
|
130
|
+
const r = await f.query({ has_links: true }); f.leak(r); expect(r.map((n: any) => n.id)).not.toContain("work-src");
|
|
131
|
+
} },
|
|
132
|
+
{ name: "has_links false includes hidden-only edge", run: async (f) => {
|
|
133
|
+
const r = await f.query({ has_links: false }); f.leak(r); expect(r.map((n: any) => n.id)).toContain("work-src");
|
|
134
|
+
} },
|
|
135
|
+
{ name: "has_links aggregate counts visible edges", run: async (f) => {
|
|
136
|
+
const r = await f.query({ has_links: true, aggregate: { op: "count" } }); f.leak(r); expect(r).toEqual([{ group: null, value: 0 }]);
|
|
137
|
+
} },
|
|
138
|
+
{ name: "content link to hidden target is broken", run: async (f) => {
|
|
139
|
+
const r = await f.query({ id: "work-src", include_broken_links: true }); f.leak(r); expect(r.broken_links).toEqual([{ target: "Dup", relationship: "wikilink" }]);
|
|
140
|
+
// A visible same-named candidate prevents brokenness, but PR1 must not
|
|
141
|
+
// synthesize an edge to it. Force the stale resolved-row state explicitly.
|
|
142
|
+
await f.store.createNote("visible dup", { id: "visible-dup", path: "wdup/Dup", tags: ["work"] });
|
|
143
|
+
f.store.db.prepare("DELETE FROM links WHERE source_id = ?").run("work-src");
|
|
144
|
+
await f.store.createLink("work-src", f.hidden.id, "wikilink");
|
|
145
|
+
const stale = await f.query({ id: "work-src", include_broken_links: true, include_link_count: true, include_links: true });
|
|
146
|
+
f.leak(stale); expect(stale.broken_links).toEqual([]); expect(stale.linkCount).toBe(0); expect(stale.links).toEqual([]);
|
|
147
|
+
} },
|
|
148
|
+
{ name: "has_broken_links true includes hidden target", run: async (f) => {
|
|
149
|
+
const r = await f.query({ has_broken_links: true }); f.leak(r); expect(r.map((n: any) => n.id)).toContain("work-src");
|
|
150
|
+
} },
|
|
151
|
+
{ name: "has_broken_links false excludes hidden target", run: async (f) => {
|
|
152
|
+
const r = await f.query({ has_broken_links: false }); f.leak(r); expect(r.map((n: any) => n.id)).not.toContain("work-src");
|
|
153
|
+
} },
|
|
154
|
+
{ name: "structured hidden edge suppressed without naming it broken", shape: "structured", run: async (f) => {
|
|
155
|
+
const r = await f.query({ id: "work-src", include_link_count: true, include_broken_links: true, include_links: true }); f.leak(r);
|
|
156
|
+
expect(r.broken_links).toEqual([]); expect(r.links).toEqual([]); expect(r.linkCount).toBe(0);
|
|
157
|
+
expect(await f.query({ has_links: true })).toEqual([]);
|
|
158
|
+
expect((await f.query({ has_links: false })).map((n: any) => n.id)).toContain("work-src");
|
|
159
|
+
} },
|
|
160
|
+
{ name: "confirming include_links and near stay walled", run: async (f) => {
|
|
161
|
+
const r = await f.query({ id: "work-src", include_links: true }); f.leak(r); expect(r.links).toEqual([]);
|
|
162
|
+
const near = await f.query({ near: { note_id: "work-src", depth: 1 } }); f.leak(near);
|
|
163
|
+
if (f.door === "MCP") { const path = await f.find("p1/Dup"); f.leak(path); expect(path).toBeNull(); }
|
|
164
|
+
} },
|
|
165
|
+
{ name: "confirming hidden intermediate path stays null", shape: "F3", run: async (f) => {
|
|
166
|
+
const r = await f.find("work-tgt"); f.leak(r); expect(r).toBeNull();
|
|
167
|
+
} },
|
|
168
|
+
];
|
|
169
|
+
|
|
38
170
|
describe("BunStore", async () => {
|
|
39
171
|
test("creates and retrieves a note", async () => {
|
|
40
172
|
const note = await store.createNote("Hello world");
|
|
@@ -1175,6 +1307,13 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1175
1307
|
// baseline scoped MCP tests above; here we only assert the *scoped*
|
|
1176
1308
|
// path's deltas.
|
|
1177
1309
|
|
|
1310
|
+
for (const pin of edgeScopedPins) {
|
|
1311
|
+
test(`vault#714 MCP: ${pin.name}`, async () => {
|
|
1312
|
+
const f = await edgeSuppressionFixture("MCP", `MCP-${pin.name}`, true, pin.shape);
|
|
1313
|
+
try { await pin.run(f); } finally { f.close(); }
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1178
1317
|
function authForTags(tags: string[]) {
|
|
1179
1318
|
return {
|
|
1180
1319
|
scopes: ["vault:read", "vault:write", "vault:admin"],
|
|
@@ -4903,6 +5042,71 @@ describe("REST transport hardening — malformed/wrong-shape/oversize JSON bodie
|
|
|
4903
5042
|
// security assertion MUST fail without the fix.
|
|
4904
5043
|
// ---------------------------------------------------------------------------
|
|
4905
5044
|
describe("HTTP tag-scope confidentiality (security review)", async () => {
|
|
5045
|
+
for (const pin of edgeScopedPins) {
|
|
5046
|
+
test(`vault#714 REST: ${pin.name}`, async () => {
|
|
5047
|
+
const f = await edgeSuppressionFixture("REST", `REST-${pin.name}`, true, pin.shape);
|
|
5048
|
+
try { await pin.run(f); } finally { f.close(); }
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
test("vault#714 REST: resolved hidden wikilink appears in unresolved listing", async () => {
|
|
5053
|
+
const f = await edgeSuppressionFixture("REST", "unresolved");
|
|
5054
|
+
try {
|
|
5055
|
+
const r = await f.unresolved(); f.leak(r);
|
|
5056
|
+
expect(r.unresolved).toEqual([{ source_id: "work-src", source_path: "work-src", target_path: "Dup", relationship: "wikilink" }]);
|
|
5057
|
+
expect(r.count).toBe(1);
|
|
5058
|
+
} finally { f.close(); }
|
|
5059
|
+
});
|
|
5060
|
+
test("vault#714 REST: confirming graph and hidden endpoint not-found identity", async () => {
|
|
5061
|
+
const f = await edgeSuppressionFixture("REST", "graph-endpoint");
|
|
5062
|
+
try {
|
|
5063
|
+
const graph = await f.query({ format: "graph", include_links: true }); f.leak(graph); expect(graph.edges).toEqual([]);
|
|
5064
|
+
// Same caller string before/after deletion, so the entire body must match.
|
|
5065
|
+
const req = () => mkReq("GET", "/find-path?source=work-src&target=Dup");
|
|
5066
|
+
const hidden = await handleFindPath(req(), f.store, f.scope);
|
|
5067
|
+
expect(hidden.status).toBe(404);
|
|
5068
|
+
const before = await hidden.text(); f.leak(JSON.parse(before));
|
|
5069
|
+
expect(JSON.parse(before)).toEqual({ error: 'Note not found: "Dup"', error_type: "not_found", note_id: "Dup" });
|
|
5070
|
+
await f.store.deleteNote(f.hidden.id);
|
|
5071
|
+
const absent = await handleFindPath(req(), f.store, f.scope);
|
|
5072
|
+
expect(absent.status).toBe(404); expect(await absent.text()).toBe(before);
|
|
5073
|
+
} finally { f.close(); }
|
|
5074
|
+
});
|
|
5075
|
+
|
|
5076
|
+
for (const semantic of [false, true]) {
|
|
5077
|
+
for (const wanted of [false, true]) {
|
|
5078
|
+
test(`vault#714 REST: ${semantic ? "semantic" : "search"} has_links=${wanted}`, async () => {
|
|
5079
|
+
const f = await edgeSuppressionFixture("REST", `search-${semantic}-${wanted}`, true, "F1", semantic);
|
|
5080
|
+
try {
|
|
5081
|
+
const mode = semantic ? { semantic: true, near_text: "src" } : { search: "src" };
|
|
5082
|
+
const r = await f.query({ ...mode, has_links: wanted }); f.leak(r);
|
|
5083
|
+
expect(r.map((n: any) => n.id)).toEqual(wanted ? [] : ["work-src"]);
|
|
5084
|
+
} finally { f.close(); }
|
|
5085
|
+
});
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
test("vault#714 REST: search has_ambiguous_links narrows hidden collision", async () => {
|
|
5089
|
+
const f = await edgeSuppressionFixture("REST", "search-ambiguity");
|
|
5090
|
+
try {
|
|
5091
|
+
await f.store.createNote("work dup", { id: "visible-dup", path: "wdup/Dup", tags: ["work"] });
|
|
5092
|
+
await f.store.deleteNote("work-src");
|
|
5093
|
+
await f.store.createNote("src [[Dup]]", { id: "work-src", path: "work-src", tags: ["work"] });
|
|
5094
|
+
const r = await f.query({ search: "src", has_ambiguous_links: true }); f.leak(r);
|
|
5095
|
+
expect(r).toEqual([]);
|
|
5096
|
+
} finally { f.close(); }
|
|
5097
|
+
});
|
|
5098
|
+
test("vault#714 REST: search has_broken_links false excludes hidden-only candidates", async () => {
|
|
5099
|
+
const f = await edgeSuppressionFixture("REST", "search-broken");
|
|
5100
|
+
try {
|
|
5101
|
+
// Existing #712 ambiguous-to-broken behavior, independent of PR1's fold.
|
|
5102
|
+
await f.store.createNote("another hidden dup", { path: "p2/Dup", tags: ["personal"] });
|
|
5103
|
+
await f.store.deleteNote("work-src");
|
|
5104
|
+
await f.store.createNote("src [[Dup]]", { id: "work-src", path: "work-src", tags: ["work"] });
|
|
5105
|
+
const r = await f.query({ search: "src", has_broken_links: false }); f.leak(r);
|
|
5106
|
+
expect(r).toEqual([]);
|
|
5107
|
+
} finally { f.close(); }
|
|
5108
|
+
});
|
|
5109
|
+
|
|
4906
5110
|
// Build a TagScopeCtx the same way routing.ts does, so handlers see the
|
|
4907
5111
|
// exact shape a real tag-scoped request produces.
|
|
4908
5112
|
async function scopeCtx(roots: string[]): Promise<TagScopeCtx> {
|
|
@@ -9159,3 +9363,41 @@ describe("handleVault: front-door structural map", async () => {
|
|
|
9159
9363
|
});
|
|
9160
9364
|
});
|
|
9161
9365
|
|
|
9366
|
+
|
|
9367
|
+
|
|
9368
|
+
describe("vault#714 unscoped full-response controls", () => {
|
|
9369
|
+
const surfaces = ["include_links", "include_link_count", "has_links", "include_broken_links", "near", "find-path", "unresolved-wikilinks", "graph"];
|
|
9370
|
+
for (const surface of surfaces) {
|
|
9371
|
+
test(`vault#714 UNSCOPED: ${surface}`, async () => {
|
|
9372
|
+
const actual: Record<string, unknown> = {};
|
|
9373
|
+
for (const door of ["MCP", "REST"] as const) {
|
|
9374
|
+
if (door === "MCP" && ["unresolved-wikilinks", "graph"].includes(surface)) continue;
|
|
9375
|
+
const f = await edgeSuppressionFixture(door, `control-${surface}`, false);
|
|
9376
|
+
try {
|
|
9377
|
+
if (surface === "find-path") actual[door] = await f.find("p1/Dup");
|
|
9378
|
+
else if (surface === "unresolved-wikilinks") actual[door] = await f.unresolved();
|
|
9379
|
+
else if (surface === "graph") actual[door] = await f.query({ format: "graph", include_links: true });
|
|
9380
|
+
else if (surface === "near") actual[door] = await f.query({ near: { note_id: "work-src", depth: 1 } });
|
|
9381
|
+
else if (surface === "has_links") actual[door] = await Promise.all([
|
|
9382
|
+
f.query({ has_links: true }), f.query({ has_links: false }),
|
|
9383
|
+
f.query({ has_links: true, aggregate: { op: "count" } }),
|
|
9384
|
+
]);
|
|
9385
|
+
else if (surface === "include_broken_links") actual[door] = await Promise.all([
|
|
9386
|
+
f.query({ id: "work-src", include_broken_links: true }),
|
|
9387
|
+
f.query({ has_broken_links: true }), f.query({ has_broken_links: false }),
|
|
9388
|
+
]);
|
|
9389
|
+
else if (surface === "include_link_count") actual[door] = await Promise.all(
|
|
9390
|
+
["both", "outbound", "inbound"].flatMap((direction) => [
|
|
9391
|
+
f.query({ id: "work-src", include_link_count: true, link_count_direction: direction }),
|
|
9392
|
+
f.query({ include_link_count: true, link_count_direction: direction }),
|
|
9393
|
+
]),
|
|
9394
|
+
);
|
|
9395
|
+
else actual[door] = await f.query({ id: "work-src", include_links: true });
|
|
9396
|
+
} finally { f.close(); }
|
|
9397
|
+
}
|
|
9398
|
+
const fixture = new URL(`./test-support/vault-714-${surface}.json`, import.meta.url);
|
|
9399
|
+
// Full JSON captured on unmodified production base 20d0f6f (Step 0).
|
|
9400
|
+
expect(JSON.parse(JSON.stringify(actual))).toEqual(JSON.parse(readFileSync(fixture, "utf8")));
|
|
9401
|
+
});
|
|
9402
|
+
}
|
|
9403
|
+
});
|