@immediately-run/grove 0.1.4 → 0.1.7
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/llms.txt +2 -1
- package/package.json +3 -3
- package/src/App.tsx +21 -19
- package/src/GroveApp.css +21 -4
- package/src/GroveWiki.entryGate.test.tsx +216 -0
- package/src/GroveWiki.tsx +60 -48
- package/src/components/BootMessage.tsx +11 -0
- package/src/components/ContentTheme.test.tsx +1 -1
- package/src/components/ContentTheme.tsx +5 -14
- package/src/components/GroveAgent.test.tsx +130 -91
- package/src/components/GroveAgent.tsx +53 -11
- package/src/components/GroveAgent.unanswered.test.tsx +45 -0
- package/src/hooks/useBundleMetadata.ts +40 -23
- package/src/hooks/useCatalogAnswered.late.test.tsx +59 -0
- package/src/hooks/useCatalogAnswered.test.tsx +89 -0
- package/src/hooks/useCatalogAnswered.ts +73 -0
- package/src/hooks/useContentComponents.ts +2 -2
- package/src/hooks/useContentStylesheets.ts +67 -0
- package/src/lib/content.test.ts +47 -1
- package/src/lib/content.ts +21 -2
- package/src/lib/contentStylesheet.test.ts +57 -1
- package/src/lib/contentStylesheet.ts +62 -4
- package/src/lib/corpusScan.test.ts +182 -2
- package/src/lib/corpusScan.ts +213 -40
- package/src/lib/corpusScanContext.ts +15 -0
- package/src/lib/criticalKeys.test.ts +82 -0
- package/src/lib/criticalKeys.ts +37 -0
- package/src/lib/entryGate.test.ts +136 -0
- package/src/lib/entryGate.ts +28 -0
- package/src/lib/layout.ts +31 -20
- package/src/lib/reachCard.test.ts +479 -61
- package/src/lib/reachCard.ts +170 -28
- package/viewer.manifest.json +1 -0
|
@@ -1,94 +1,512 @@
|
|
|
1
|
-
// R-GA-1 / G-GA-1 / G-GA-6 / G-GA-10 — the reach card is a pure function of
|
|
2
|
-
// envelope; no pixel claims a capability the session lacks, causes are distinct
|
|
3
|
-
// concrete, and state flips recompute rows AND chips.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
1
|
+
// R-GA-1 / G-GA-1 / G-GA-6 / G-GA-10 / R-SP-6 — the reach card is a pure function of
|
|
2
|
+
// the envelope; no pixel claims a capability the session lacks, causes are distinct
|
|
3
|
+
// and concrete, and state flips recompute rows AND chips. R3-752: the apply row is a
|
|
4
|
+
// destination (`elsewhere`, never ✗), the packaging substrate is its own neutral row,
|
|
5
|
+
// the source-trust copy is keyed on its basis, and no row carries a model noun.
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
computeReachRows,
|
|
9
|
+
reachChips,
|
|
10
|
+
sourceTrustLine,
|
|
11
|
+
showEgressDisclosure,
|
|
12
|
+
} from "./reachCard";
|
|
13
|
+
import type { ChatProviderState } from "@immediately-run/sdk";
|
|
7
14
|
|
|
8
15
|
const configured = (tools = true): ChatProviderState => ({
|
|
9
|
-
status:
|
|
10
|
-
provider: {
|
|
16
|
+
status: "configured",
|
|
17
|
+
provider: {
|
|
18
|
+
providerId: "llm.chat.anthropic",
|
|
19
|
+
hostVouched: true,
|
|
20
|
+
features: {
|
|
21
|
+
vision: false,
|
|
22
|
+
tools,
|
|
23
|
+
jsonMode: true,
|
|
24
|
+
reasoning: false,
|
|
25
|
+
maxContextTokens: 100000,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
11
28
|
});
|
|
12
29
|
|
|
13
|
-
const row = (rows: ReturnType<typeof computeReachRows>, key: string) =>
|
|
30
|
+
const row = (rows: ReturnType<typeof computeReachRows>, key: string) =>
|
|
31
|
+
rows.find((r) => r.key === key)!;
|
|
14
32
|
|
|
15
|
-
describe(
|
|
16
|
-
it(
|
|
17
|
-
const rows = computeReachRows({
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
describe("G-GA-10 — the Q&A row renders the provider state honestly", () => {
|
|
34
|
+
it("unknown renders NEUTRAL — no cause, no connect copy (the R3-300 rule)", () => {
|
|
35
|
+
const rows = computeReachRows({
|
|
36
|
+
providerState: { status: "unknown" },
|
|
37
|
+
chatGranted: true,
|
|
38
|
+
catalogAnswered: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
sourceShared: true,
|
|
41
|
+
mountId: null,
|
|
42
|
+
toolsSupported: true,
|
|
43
|
+
});
|
|
44
|
+
expect(row(rows, "answer").state).toBe("neutral");
|
|
45
|
+
expect(row(rows, "answer").cause).toBeUndefined();
|
|
20
46
|
});
|
|
21
47
|
|
|
22
|
-
it(
|
|
23
|
-
const noKey = computeReachRows({
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
it("not-configured names the KEY cause; ungranted names the CONSENT cause — never conflated", () => {
|
|
49
|
+
const noKey = computeReachRows({
|
|
50
|
+
providerState: { status: "not-configured" },
|
|
51
|
+
chatGranted: true,
|
|
52
|
+
catalogAnswered: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
sourceShared: false,
|
|
55
|
+
mountId: null,
|
|
56
|
+
toolsSupported: true,
|
|
57
|
+
});
|
|
58
|
+
expect(row(noKey, "answer").state).toBe("blocked");
|
|
59
|
+
expect(row(noKey, "answer").cause).toContain("add one in Settings");
|
|
26
60
|
|
|
27
|
-
const forbidden = computeReachRows({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
const forbidden = computeReachRows({
|
|
62
|
+
providerState: configured(),
|
|
63
|
+
chatGranted: false,
|
|
64
|
+
catalogAnswered: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
sourceShared: false,
|
|
67
|
+
mountId: null,
|
|
68
|
+
toolsSupported: true,
|
|
69
|
+
});
|
|
70
|
+
expect(row(forbidden, "answer").state).toBe("blocked");
|
|
71
|
+
expect(row(forbidden, "answer").cause).toContain("wasn't granted chat");
|
|
72
|
+
expect(row(forbidden, "answer").cause).not.toContain("Settings");
|
|
31
73
|
});
|
|
32
74
|
|
|
33
|
-
it(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
75
|
+
it("the host-marked UNGRANTED state names the consent cause WITHOUT a configured provider (R3-688)", () => {
|
|
76
|
+
// The R3-688 exit: an ungranted fork is never told the provider, so before the
|
|
77
|
+
// host mark the consent cause was uncomputable — the fork read not-configured and
|
|
78
|
+
// rendered the KEY copy at a user who had one. The marked state fixes the cause
|
|
79
|
+
// with nothing but the grant decision.
|
|
80
|
+
const marked = computeReachRows({
|
|
81
|
+
providerState: { status: "ungranted" },
|
|
82
|
+
chatGranted: false,
|
|
83
|
+
catalogAnswered: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
sourceShared: false,
|
|
86
|
+
mountId: null,
|
|
87
|
+
toolsSupported: true,
|
|
88
|
+
});
|
|
89
|
+
expect(row(marked, "answer").state).toBe("blocked");
|
|
90
|
+
expect(row(marked, "answer").cause).toContain("wasn't granted chat");
|
|
91
|
+
expect(row(marked, "answer").cause).not.toContain("Settings");
|
|
92
|
+
// The egress line stays hidden: no provider is bound for this frame.
|
|
93
|
+
expect(showEgressDisclosure({ status: "ungranted" })).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("the mark ALONE blocks — ungranted with a granted-looking catalog (R3-688, isolating)", () => {
|
|
97
|
+
// THE test for this item, and it was missing: every other ungranted case also sets
|
|
98
|
+
// `chatGranted: false`, so `!chatGranted` satisfies them on its own and the whole
|
|
99
|
+
// R3-688 arm could be deleted with the suite green. Round 1 proved that by deleting
|
|
100
|
+
// `providerState.status === 'ungranted' ||` — 499/499 still passed.
|
|
101
|
+
//
|
|
102
|
+
// This is the one input that isolates the mark: a stale or racing catalog still
|
|
103
|
+
// advertising `llm:chat` beside a host that has marked the answer ungranted.
|
|
104
|
+
//
|
|
105
|
+
// What deleting the arm does now, measured: the input falls into the exhaustiveness
|
|
106
|
+
// branch and renders `neutral`, so line 59's state assertion is what catches it. It
|
|
107
|
+
// is `'ok'` WITH both chips — an unbacked capability claim, not merely a wrong cause
|
|
108
|
+
// — only before the tail was narrowed in the same commit, which is the state round 1
|
|
109
|
+
// found. Both are caught; the chip assertions below hold the stronger, older shape.
|
|
110
|
+
const marked = computeReachRows({
|
|
111
|
+
providerState: { status: "ungranted" },
|
|
112
|
+
chatGranted: true,
|
|
113
|
+
catalogAnswered: true,
|
|
114
|
+
writable: true,
|
|
115
|
+
sourceShared: false,
|
|
116
|
+
mountId: null,
|
|
117
|
+
toolsSupported: true,
|
|
118
|
+
});
|
|
119
|
+
expect(row(marked, "answer").state).toBe("blocked");
|
|
120
|
+
expect(row(marked, "answer").cause).toContain("wasn't granted chat");
|
|
121
|
+
expect(row(marked, "answer").cause).not.toContain("Settings");
|
|
122
|
+
expect(row(marked, "answer").chips).toBeUndefined();
|
|
123
|
+
expect(
|
|
124
|
+
reachChips(marked).some((c) => /summarize|tagged security/i.test(c)),
|
|
125
|
+
).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("an UNANSWERED catalog names no cause — neutral, not a guessed consent state", () => {
|
|
129
|
+
// `!chatGranted` is true both for "not granted" and for "the host has not replied",
|
|
130
|
+
// and the catalog's value cannot separate them (an empty catalog is a real answer).
|
|
131
|
+
// Guessing "not granted" claims a consent state about a frame that may hold the
|
|
132
|
+
// grant; guessing the ✓ arm claims a capability it may not have, which R-GA-1
|
|
133
|
+
// forbids outright. So: name nothing, exactly as `unknown` does one channel over.
|
|
134
|
+
const waiting = computeReachRows({
|
|
135
|
+
providerState: { status: "not-configured" },
|
|
136
|
+
chatGranted: false,
|
|
137
|
+
catalogAnswered: false,
|
|
138
|
+
writable: true,
|
|
139
|
+
sourceShared: false,
|
|
140
|
+
mountId: null,
|
|
141
|
+
toolsSupported: true,
|
|
142
|
+
});
|
|
143
|
+
expect(row(waiting, "answer").state).toBe("neutral");
|
|
144
|
+
expect(row(waiting, "answer").cause).toBeUndefined();
|
|
145
|
+
expect(row(waiting, "answer").chips).toBeUndefined();
|
|
146
|
+
|
|
147
|
+
// …and a CONFIGURED provider must not fall through to ✓ while the grant is unknown.
|
|
148
|
+
const keyed = computeReachRows({
|
|
149
|
+
providerState: configured(),
|
|
150
|
+
chatGranted: false,
|
|
151
|
+
catalogAnswered: false,
|
|
152
|
+
writable: true,
|
|
153
|
+
sourceShared: false,
|
|
154
|
+
mountId: null,
|
|
155
|
+
toolsSupported: true,
|
|
156
|
+
});
|
|
157
|
+
expect(row(keyed, "answer").state).toBe("neutral");
|
|
158
|
+
expect(
|
|
159
|
+
reachChips(keyed).some((c) => /summarize|tagged security/i.test(c)),
|
|
160
|
+
).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("the host MARK does not wait for the catalog — ungranted blocks with no answer at all", () => {
|
|
164
|
+
// The mark is authoritative on its own; gating it behind the catalog would reintroduce
|
|
165
|
+
// the very gap R3-688 closes.
|
|
166
|
+
const marked = computeReachRows({
|
|
167
|
+
providerState: { status: "ungranted" },
|
|
168
|
+
chatGranted: false,
|
|
169
|
+
catalogAnswered: false,
|
|
170
|
+
writable: true,
|
|
171
|
+
sourceShared: false,
|
|
172
|
+
mountId: null,
|
|
173
|
+
toolsSupported: true,
|
|
174
|
+
});
|
|
175
|
+
expect(row(marked, "answer").state).toBe("blocked");
|
|
176
|
+
expect(row(marked, "answer").cause).toContain("wasn't granted chat");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("configured + granted is ✓ and carries the read-flavored chips", () => {
|
|
180
|
+
const ok = computeReachRows({
|
|
181
|
+
providerState: configured(),
|
|
182
|
+
chatGranted: true,
|
|
183
|
+
catalogAnswered: true,
|
|
184
|
+
writable: false,
|
|
185
|
+
sourceShared: false,
|
|
186
|
+
mountId: null,
|
|
187
|
+
toolsSupported: true,
|
|
188
|
+
});
|
|
189
|
+
expect(row(ok, "answer").state).toBe("ok");
|
|
190
|
+
expect(row(ok, "answer").chips).toBeDefined();
|
|
37
191
|
});
|
|
38
192
|
});
|
|
39
193
|
|
|
40
|
-
describe(
|
|
41
|
-
it(
|
|
42
|
-
const rows = computeReachRows({
|
|
43
|
-
|
|
44
|
-
|
|
194
|
+
describe("G-GA-1 — the read/draft/apply rows derive from writability, never copy", () => {
|
|
195
|
+
it("read-only blocks Draft with the reader cause and yields NO write chip", () => {
|
|
196
|
+
const rows = computeReachRows({
|
|
197
|
+
providerState: configured(),
|
|
198
|
+
chatGranted: true,
|
|
199
|
+
catalogAnswered: true,
|
|
200
|
+
writable: false,
|
|
201
|
+
sourceShared: false,
|
|
202
|
+
mountId: null,
|
|
203
|
+
toolsSupported: true,
|
|
204
|
+
});
|
|
205
|
+
expect(row(rows, "draft").state).toBe("blocked");
|
|
206
|
+
expect(row(rows, "draft").cause).toContain("reader");
|
|
45
207
|
const chips = reachChips(rows);
|
|
46
|
-
expect(
|
|
208
|
+
expect(
|
|
209
|
+
chips.some((c) =>
|
|
210
|
+
/add an entry|fix broken|reorganize|timeline|suggest an edit/i.test(c),
|
|
211
|
+
),
|
|
212
|
+
).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("writable turns Draft ✓ — and its chip is a DESCRIBE, not an apply", () => {
|
|
216
|
+
const rows = computeReachRows({
|
|
217
|
+
providerState: configured(),
|
|
218
|
+
chatGranted: true,
|
|
219
|
+
catalogAnswered: true,
|
|
220
|
+
writable: true,
|
|
221
|
+
sourceShared: false,
|
|
222
|
+
mountId: null,
|
|
223
|
+
toolsSupported: true,
|
|
224
|
+
});
|
|
225
|
+
expect(row(rows, "draft").state).toBe("ok");
|
|
226
|
+
expect((row(rows, "draft").chips ?? []).join(" ")).toMatch(
|
|
227
|
+
/suggest an edit/i,
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("R3-752 — the apply row is a destination, across every input (R-GA-3)", () => {
|
|
233
|
+
// Every member of the union, including the one R3-688 adds — the spec calls this sweep
|
|
234
|
+
// the mechanical guarantee for R-SP-6, and it was enumerating three of four, missing
|
|
235
|
+
// exactly the state this item is about.
|
|
236
|
+
const states: ChatProviderState[] = [
|
|
237
|
+
{ status: "unknown" },
|
|
238
|
+
{ status: "not-configured" },
|
|
239
|
+
{ status: "ungranted" },
|
|
240
|
+
configured(true),
|
|
241
|
+
configured(false),
|
|
242
|
+
];
|
|
243
|
+
// The full cross-product of {writable} × {sourceShared} × {provider states}: NO input
|
|
244
|
+
// can put the apply row back to blocked or put the trust sentence back on it.
|
|
245
|
+
for (const writable of [true, false]) {
|
|
246
|
+
for (const sourceShared of [true, false]) {
|
|
247
|
+
for (const providerState of states) {
|
|
248
|
+
it(`writable=${writable} sourceShared=${sourceShared} provider=${providerState.status} → elsewhere with a destination and no cause`, () => {
|
|
249
|
+
const rows = computeReachRows({
|
|
250
|
+
providerState,
|
|
251
|
+
chatGranted: true,
|
|
252
|
+
catalogAnswered: true,
|
|
253
|
+
writable,
|
|
254
|
+
sourceShared,
|
|
255
|
+
mountId: "space:abc",
|
|
256
|
+
toolsSupported: true,
|
|
257
|
+
});
|
|
258
|
+
expect(row(rows, "apply").state).toBe("elsewhere");
|
|
259
|
+
expect(row(rows, "apply").destination).toContain(
|
|
260
|
+
"editor or workbench",
|
|
261
|
+
);
|
|
262
|
+
expect(row(rows, "apply").cause).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it("the source-trust sentence is a property of the SOURCE — never on the apply row", () => {
|
|
269
|
+
const rows = computeReachRows({
|
|
270
|
+
providerState: configured(),
|
|
271
|
+
chatGranted: true,
|
|
272
|
+
catalogAnswered: true,
|
|
273
|
+
writable: true,
|
|
274
|
+
sourceShared: true,
|
|
275
|
+
mountId: null,
|
|
276
|
+
toolsSupported: true,
|
|
277
|
+
});
|
|
278
|
+
expect(JSON.stringify(row(rows, "apply"))).not.toContain("others can");
|
|
279
|
+
expect(JSON.stringify(row(rows, "apply"))).not.toContain(
|
|
280
|
+
"push to this repo",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("R3-752 — the packaging row states the substrate, as context (neutral, no chips)", () => {
|
|
286
|
+
it("mountId: null is the own-corpus row; an id is the mounted row", () => {
|
|
287
|
+
const own = computeReachRows({
|
|
288
|
+
providerState: configured(),
|
|
289
|
+
chatGranted: true,
|
|
290
|
+
catalogAnswered: true,
|
|
291
|
+
writable: true,
|
|
292
|
+
sourceShared: true,
|
|
293
|
+
mountId: null,
|
|
294
|
+
toolsSupported: true,
|
|
295
|
+
});
|
|
296
|
+
expect(row(own, "packaging").label).toBe("Reads its own entries");
|
|
297
|
+
expect(row(own, "packaging").state).toBe("neutral");
|
|
298
|
+
|
|
299
|
+
const mounted = computeReachRows({
|
|
300
|
+
providerState: configured(),
|
|
301
|
+
chatGranted: true,
|
|
302
|
+
catalogAnswered: true,
|
|
303
|
+
writable: true,
|
|
304
|
+
sourceShared: true,
|
|
305
|
+
mountId: "space:abc",
|
|
306
|
+
toolsSupported: true,
|
|
307
|
+
});
|
|
308
|
+
expect(row(mounted, "packaging").label).toBe(
|
|
309
|
+
"Reads a wiki mounted into it",
|
|
310
|
+
);
|
|
311
|
+
expect(row(mounted, "packaging").state).toBe("neutral");
|
|
47
312
|
});
|
|
48
313
|
|
|
49
|
-
it(
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
314
|
+
it("the packaging row contributes no chips — either packaging, same chip set", () => {
|
|
315
|
+
const own = reachChips(
|
|
316
|
+
computeReachRows({
|
|
317
|
+
providerState: configured(),
|
|
318
|
+
chatGranted: true,
|
|
319
|
+
catalogAnswered: true,
|
|
320
|
+
writable: true,
|
|
321
|
+
sourceShared: true,
|
|
322
|
+
mountId: null,
|
|
323
|
+
toolsSupported: true,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
const mounted = reachChips(
|
|
327
|
+
computeReachRows({
|
|
328
|
+
providerState: configured(),
|
|
329
|
+
chatGranted: true,
|
|
330
|
+
catalogAnswered: true,
|
|
331
|
+
writable: true,
|
|
332
|
+
sourceShared: true,
|
|
333
|
+
mountId: "space:abc",
|
|
334
|
+
toolsSupported: true,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
expect(mounted).toEqual(own);
|
|
53
338
|
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("R3-752 — the source-trust line is keyed on WHY the source reads as shared", () => {
|
|
342
|
+
it("the two bases produce different strings, neither on a row", () => {
|
|
343
|
+
const git = sourceTrustLine(true, "git-indeterminate");
|
|
344
|
+
const mount = sourceTrustLine(true, "mount-trust-mode");
|
|
345
|
+
expect(git).toContain("push to this repo");
|
|
346
|
+
expect(mount).toContain("others can change");
|
|
347
|
+
expect(git).not.toBe(mount);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("a source that is not shared renders NO line — no reassurance about a regime not running", () => {
|
|
351
|
+
expect(sourceTrustLine(false, "git-indeterminate")).toBeNull();
|
|
352
|
+
expect(sourceTrustLine(false, "mount-trust-mode")).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
54
355
|
|
|
55
|
-
|
|
356
|
+
describe("R3-752 — R-SP-6: no model nouns anywhere on the card, checked mechanically", () => {
|
|
357
|
+
// Every member of the union, including the one R3-688 adds — the spec calls this sweep
|
|
358
|
+
// the mechanical guarantee for R-SP-6, and it was enumerating three of four, missing
|
|
359
|
+
// exactly the state this item is about.
|
|
360
|
+
const states: ChatProviderState[] = [
|
|
361
|
+
{ status: "unknown" },
|
|
362
|
+
{ status: "not-configured" },
|
|
363
|
+
{ status: "ungranted" },
|
|
364
|
+
configured(true),
|
|
365
|
+
configured(false),
|
|
366
|
+
];
|
|
367
|
+
const banned =
|
|
368
|
+
/\b(taint|tainted|tier|principal|trust mode|capability|grant|shared source|low-trust)\b/i;
|
|
369
|
+
|
|
370
|
+
it("no string any row produces matches the banned vocabulary, over the full cross-product", () => {
|
|
56
371
|
for (const writable of [true, false]) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
372
|
+
for (const sourceShared of [true, false]) {
|
|
373
|
+
for (const providerState of states) {
|
|
374
|
+
for (const mountId of [null, "space:abc"]) {
|
|
375
|
+
for (const toolsSupported of [true, false]) {
|
|
376
|
+
for (const chatGranted of [true, false]) {
|
|
377
|
+
for (const catalogAnswered of [true, false]) {
|
|
378
|
+
const rows = computeReachRows({
|
|
379
|
+
providerState,
|
|
380
|
+
chatGranted,
|
|
381
|
+
catalogAnswered,
|
|
382
|
+
writable,
|
|
383
|
+
sourceShared,
|
|
384
|
+
mountId,
|
|
385
|
+
toolsSupported,
|
|
386
|
+
});
|
|
387
|
+
const strings = rows.flatMap((r) =>
|
|
388
|
+
[
|
|
389
|
+
r.label,
|
|
390
|
+
r.cause,
|
|
391
|
+
r.destination,
|
|
392
|
+
...(r.chips ?? []),
|
|
393
|
+
].filter((s): s is string => typeof s === "string"),
|
|
394
|
+
);
|
|
395
|
+
for (const s of strings) expect(s).not.toMatch(banned);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("the source-trust lines pass the same vocabulary rule", () => {
|
|
406
|
+
for (const basis of ["git-indeterminate", "mount-trust-mode"] as const) {
|
|
407
|
+
const line = sourceTrustLine(true, basis)!;
|
|
408
|
+
expect(line).not.toMatch(banned);
|
|
60
409
|
}
|
|
61
410
|
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("R3-752 / G-GA-8 — the tool-less degrade is a visible qualifier, never a silent downgrade", () => {
|
|
414
|
+
it("configured + granted + tools=false keeps row 1 ✓ and adds the degrade qualifier", () => {
|
|
415
|
+
const degraded = computeReachRows({
|
|
416
|
+
providerState: configured(false),
|
|
417
|
+
chatGranted: true,
|
|
418
|
+
catalogAnswered: true,
|
|
419
|
+
writable: true,
|
|
420
|
+
sourceShared: true,
|
|
421
|
+
mountId: null,
|
|
422
|
+
toolsSupported: false,
|
|
423
|
+
});
|
|
424
|
+
expect(row(degraded, "answer").state).toBe("ok");
|
|
425
|
+
expect(row(degraded, "answer").label).toContain(
|
|
426
|
+
"reads a summary of this wiki",
|
|
427
|
+
);
|
|
428
|
+
expect(row(degraded, "answer").chips).toBeDefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("tools=true restores the plain label; a blocked row never grows the qualifier", () => {
|
|
432
|
+
const fine = computeReachRows({
|
|
433
|
+
providerState: configured(true),
|
|
434
|
+
chatGranted: true,
|
|
435
|
+
catalogAnswered: true,
|
|
436
|
+
writable: true,
|
|
437
|
+
sourceShared: true,
|
|
438
|
+
mountId: null,
|
|
439
|
+
toolsSupported: true,
|
|
440
|
+
});
|
|
441
|
+
expect(row(fine, "answer").label).toBe("Answer questions about this wiki");
|
|
62
442
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
443
|
+
const noKey = computeReachRows({
|
|
444
|
+
providerState: { status: "not-configured" },
|
|
445
|
+
chatGranted: true,
|
|
446
|
+
catalogAnswered: true,
|
|
447
|
+
writable: true,
|
|
448
|
+
sourceShared: true,
|
|
449
|
+
mountId: null,
|
|
450
|
+
toolsSupported: false,
|
|
451
|
+
});
|
|
452
|
+
expect(row(noKey, "answer").label).toBe("Answer questions about this wiki");
|
|
453
|
+
expect(row(noKey, "answer").cause).toContain("add one in Settings");
|
|
68
454
|
});
|
|
69
455
|
});
|
|
70
456
|
|
|
71
|
-
describe(
|
|
72
|
-
it(
|
|
73
|
-
const before = computeReachRows({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
457
|
+
describe("G-GA-6 — envelope flips recompute rows and chips together", () => {
|
|
458
|
+
it("disconnecting the provider flips answer ✓→blocked and drops its chips", () => {
|
|
459
|
+
const before = computeReachRows({
|
|
460
|
+
providerState: configured(),
|
|
461
|
+
chatGranted: true,
|
|
462
|
+
catalogAnswered: true,
|
|
463
|
+
writable: true,
|
|
464
|
+
sourceShared: false,
|
|
465
|
+
mountId: null,
|
|
466
|
+
toolsSupported: true,
|
|
467
|
+
});
|
|
468
|
+
const after = computeReachRows({
|
|
469
|
+
providerState: { status: "not-configured" },
|
|
470
|
+
chatGranted: true,
|
|
471
|
+
catalogAnswered: true,
|
|
472
|
+
writable: true,
|
|
473
|
+
sourceShared: false,
|
|
474
|
+
mountId: null,
|
|
475
|
+
toolsSupported: true,
|
|
476
|
+
});
|
|
477
|
+
expect(row(before, "answer").state).toBe("ok");
|
|
478
|
+
expect(row(after, "answer").state).toBe("blocked");
|
|
479
|
+
expect(reachChips(after)).not.toContain("Summarize this entry");
|
|
78
480
|
});
|
|
79
481
|
|
|
80
|
-
it(
|
|
81
|
-
const before = computeReachRows({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
482
|
+
it("revoking write flips draft ✓→blocked on the next computation", () => {
|
|
483
|
+
const before = computeReachRows({
|
|
484
|
+
providerState: configured(),
|
|
485
|
+
chatGranted: true,
|
|
486
|
+
catalogAnswered: true,
|
|
487
|
+
writable: true,
|
|
488
|
+
sourceShared: false,
|
|
489
|
+
mountId: null,
|
|
490
|
+
toolsSupported: true,
|
|
491
|
+
});
|
|
492
|
+
const after = computeReachRows({
|
|
493
|
+
providerState: configured(),
|
|
494
|
+
chatGranted: true,
|
|
495
|
+
catalogAnswered: true,
|
|
496
|
+
writable: false,
|
|
497
|
+
sourceShared: false,
|
|
498
|
+
mountId: null,
|
|
499
|
+
toolsSupported: true,
|
|
500
|
+
});
|
|
501
|
+
expect(row(before, "draft").state).toBe("ok");
|
|
502
|
+
expect(row(after, "draft").state).toBe("blocked");
|
|
85
503
|
});
|
|
86
504
|
});
|
|
87
505
|
|
|
88
|
-
describe(
|
|
89
|
-
it(
|
|
506
|
+
describe("R-GA-6 — the egress disclosure is unconditional when a provider is bound", () => {
|
|
507
|
+
it("shows for configured, not for unknown or not-configured, whatever the trust mode", () => {
|
|
90
508
|
expect(showEgressDisclosure(configured())).toBe(true);
|
|
91
|
-
expect(showEgressDisclosure({ status:
|
|
92
|
-
expect(showEgressDisclosure({ status:
|
|
509
|
+
expect(showEgressDisclosure({ status: "unknown" })).toBe(false);
|
|
510
|
+
expect(showEgressDisclosure({ status: "not-configured" })).toBe(false);
|
|
93
511
|
});
|
|
94
512
|
});
|