@stigmer/react 3.11.1-dev.20260812192248 → 3.12.0
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/activity/useRecentActivity.d.ts +3 -2
- package/activity/useRecentActivity.d.ts.map +1 -1
- package/activity/useRecentActivity.js +3 -2
- package/activity/useRecentActivity.js.map +1 -1
- package/agent/AgentDetailView.d.ts +2 -12
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +10 -42
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/internal/agentToInput.d.ts.map +1 -1
- package/agent/internal/agentToInput.js +0 -8
- package/agent/internal/agentToInput.js.map +1 -1
- package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
- package/dependency-graph/DependencyTreeNode.js +1 -7
- package/dependency-graph/DependencyTreeNode.js.map +1 -1
- package/dependency-graph/types.d.ts +8 -20
- package/dependency-graph/types.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.js +2 -20
- package/dependency-graph/useDependencyGraph.js.map +1 -1
- package/index.d.ts +0 -2
- package/index.d.ts.map +1 -1
- package/index.js +0 -6
- package/index.js.map +1 -1
- package/library/useExportResource.d.ts +3 -4
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/manifest/EditResourceYamlDialog.d.ts.map +1 -1
- package/manifest/EditResourceYamlDialog.js.map +1 -1
- package/package.json +4 -4
- package/src/activity/useRecentActivity.ts +3 -2
- package/src/agent/AgentDetailView.tsx +6 -130
- package/src/agent/internal/__tests__/agentToInput.test.ts +2 -11
- package/src/agent/internal/agentToInput.ts +0 -11
- package/src/dependency-graph/DependencyTreeNode.tsx +1 -7
- package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +18 -34
- package/src/dependency-graph/types.ts +8 -20
- package/src/dependency-graph/useDependencyGraph.ts +2 -22
- package/src/index.ts +0 -82
- package/src/library/useExportResource.ts +3 -4
- package/src/manifest/EditResourceYamlDialog.tsx +1 -2
- package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +2 -2
- package/src/manifest/__tests__/EditResourceYamlDialog.test.tsx +9 -9
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/datastore/CollectionRecordsBrowser.d.ts +0 -34
- package/datastore/CollectionRecordsBrowser.d.ts.map +0 -1
- package/datastore/CollectionRecordsBrowser.js +0 -226
- package/datastore/CollectionRecordsBrowser.js.map +0 -1
- package/datastore/CollectionSchemaView.d.ts +0 -29
- package/datastore/CollectionSchemaView.d.ts.map +0 -1
- package/datastore/CollectionSchemaView.js +0 -93
- package/datastore/CollectionSchemaView.js.map +0 -1
- package/datastore/DatastoreDetailView.d.ts +0 -44
- package/datastore/DatastoreDetailView.d.ts.map +0 -1
- package/datastore/DatastoreDetailView.js +0 -76
- package/datastore/DatastoreDetailView.js.map +0 -1
- package/datastore/DatastoreSyncReport.d.ts +0 -19
- package/datastore/DatastoreSyncReport.d.ts.map +0 -1
- package/datastore/DatastoreSyncReport.js +0 -45
- package/datastore/DatastoreSyncReport.js.map +0 -1
- package/datastore/DeleteDatastoreDialog.d.ts +0 -35
- package/datastore/DeleteDatastoreDialog.d.ts.map +0 -1
- package/datastore/DeleteDatastoreDialog.js +0 -90
- package/datastore/DeleteDatastoreDialog.js.map +0 -1
- package/datastore/FieldValueControl.d.ts +0 -55
- package/datastore/FieldValueControl.d.ts.map +0 -1
- package/datastore/FieldValueControl.js +0 -138
- package/datastore/FieldValueControl.js.map +0 -1
- package/datastore/RecordFilterBuilder.d.ts +0 -37
- package/datastore/RecordFilterBuilder.d.ts.map +0 -1
- package/datastore/RecordFilterBuilder.js +0 -122
- package/datastore/RecordFilterBuilder.js.map +0 -1
- package/datastore/RecordFormPanel.d.ts +0 -45
- package/datastore/RecordFormPanel.d.ts.map +0 -1
- package/datastore/RecordFormPanel.js +0 -215
- package/datastore/RecordFormPanel.js.map +0 -1
- package/datastore/index.d.ts +0 -43
- package/datastore/index.d.ts.map +0 -1
- package/datastore/index.js +0 -22
- package/datastore/index.js.map +0 -1
- package/datastore/recordFilter.d.ts +0 -62
- package/datastore/recordFilter.d.ts.map +0 -1
- package/datastore/recordFilter.js +0 -104
- package/datastore/recordFilter.js.map +0 -1
- package/datastore/recordValues.d.ts +0 -85
- package/datastore/recordValues.d.ts.map +0 -1
- package/datastore/recordValues.js +0 -282
- package/datastore/recordValues.js.map +0 -1
- package/datastore/useDatastore.d.ts +0 -39
- package/datastore/useDatastore.d.ts.map +0 -1
- package/datastore/useDatastore.js +0 -44
- package/datastore/useDatastore.js.map +0 -1
- package/datastore/useDatastoreCount.d.ts +0 -50
- package/datastore/useDatastoreCount.d.ts.map +0 -1
- package/datastore/useDatastoreCount.js +0 -27
- package/datastore/useDatastoreCount.js.map +0 -1
- package/datastore/useDatastoreDescription.d.ts +0 -54
- package/datastore/useDatastoreDescription.d.ts.map +0 -1
- package/datastore/useDatastoreDescription.js +0 -50
- package/datastore/useDatastoreDescription.js.map +0 -1
- package/datastore/useDatastoreList.d.ts +0 -57
- package/datastore/useDatastoreList.d.ts.map +0 -1
- package/datastore/useDatastoreList.js +0 -29
- package/datastore/useDatastoreList.js.map +0 -1
- package/datastore/useDatastoreSearch.d.ts +0 -19
- package/datastore/useDatastoreSearch.d.ts.map +0 -1
- package/datastore/useDatastoreSearch.js +0 -22
- package/datastore/useDatastoreSearch.js.map +0 -1
- package/datastore/useDeleteDatastore.d.ts +0 -53
- package/datastore/useDeleteDatastore.d.ts.map +0 -1
- package/datastore/useDeleteDatastore.js +0 -40
- package/datastore/useDeleteDatastore.js.map +0 -1
- package/datastore/useDeleteRecord.d.ts +0 -34
- package/datastore/useDeleteRecord.d.ts.map +0 -1
- package/datastore/useDeleteRecord.js +0 -43
- package/datastore/useDeleteRecord.js.map +0 -1
- package/datastore/useInsertRecord.d.ts +0 -50
- package/datastore/useInsertRecord.d.ts.map +0 -1
- package/datastore/useInsertRecord.js +0 -55
- package/datastore/useInsertRecord.js.map +0 -1
- package/datastore/useRecordCollection.d.ts +0 -104
- package/datastore/useRecordCollection.d.ts.map +0 -1
- package/datastore/useRecordCollection.js +0 -136
- package/datastore/useRecordCollection.js.map +0 -1
- package/datastore/useRecordList.d.ts +0 -66
- package/datastore/useRecordList.d.ts.map +0 -1
- package/datastore/useRecordList.js +0 -58
- package/datastore/useRecordList.js.map +0 -1
- package/datastore/useUpdateRecord.d.ts +0 -52
- package/datastore/useUpdateRecord.d.ts.map +0 -1
- package/datastore/useUpdateRecord.js +0 -55
- package/datastore/useUpdateRecord.js.map +0 -1
- package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +0 -166
- package/src/datastore/CollectionRecordsBrowser.tsx +0 -571
- package/src/datastore/CollectionSchemaView.tsx +0 -247
- package/src/datastore/DatastoreDetailView.tsx +0 -201
- package/src/datastore/DatastoreSyncReport.tsx +0 -105
- package/src/datastore/DeleteDatastoreDialog.tsx +0 -202
- package/src/datastore/FieldValueControl.tsx +0 -289
- package/src/datastore/RecordFilterBuilder.tsx +0 -422
- package/src/datastore/RecordFormPanel.tsx +0 -432
- package/src/datastore/__tests__/CollectionRecordsBrowser.test.tsx +0 -245
- package/src/datastore/__tests__/DeleteDatastoreDialog.test.tsx +0 -133
- package/src/datastore/__tests__/FieldValueControl.test.tsx +0 -160
- package/src/datastore/__tests__/RecordFilterBuilder.test.tsx +0 -155
- package/src/datastore/__tests__/RecordFormPanel.test.tsx +0 -227
- package/src/datastore/__tests__/recordFilter.test.ts +0 -169
- package/src/datastore/__tests__/recordMutations.test.tsx +0 -195
- package/src/datastore/__tests__/recordValues.test.ts +0 -359
- package/src/datastore/__tests__/schemaAndSyncViews.test.tsx +0 -142
- package/src/datastore/__tests__/useDatastoreDescription.test.tsx +0 -143
- package/src/datastore/__tests__/useRecordCollection.test.tsx +0 -250
- package/src/datastore/__tests__/useRecordList.test.tsx +0 -85
- package/src/datastore/index.ts +0 -111
- package/src/datastore/recordFilter.ts +0 -154
- package/src/datastore/recordValues.ts +0 -327
- package/src/datastore/useDatastore.ts +0 -67
- package/src/datastore/useDatastoreCount.ts +0 -69
- package/src/datastore/useDatastoreDescription.ts +0 -90
- package/src/datastore/useDatastoreList.ts +0 -79
- package/src/datastore/useDatastoreSearch.ts +0 -39
- package/src/datastore/useDeleteDatastore.ts +0 -87
- package/src/datastore/useDeleteRecord.ts +0 -77
- package/src/datastore/useInsertRecord.ts +0 -92
- package/src/datastore/useRecordCollection.ts +0 -256
- package/src/datastore/useRecordList.ts +0 -131
- package/src/datastore/useUpdateRecord.ts +0 -95
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
// Tests for the client-side projection of the DD-004 canonical value
|
|
2
|
-
// contract. The cases mirror the authoritative Go schema tests
|
|
3
|
-
// (pkg/domain/datastore/schema) so a drift between projection and
|
|
4
|
-
// enforcement shows up here first.
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
-
import { create } from "@bufbuild/protobuf";
|
|
8
|
-
import {
|
|
9
|
-
FieldDeclarationSchema,
|
|
10
|
-
FieldType,
|
|
11
|
-
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/spec_pb";
|
|
12
|
-
import { RecordConditionOp } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_io_pb";
|
|
13
|
-
import {
|
|
14
|
-
buildUpdateFields,
|
|
15
|
-
coerceFieldValue,
|
|
16
|
-
formatFieldValue,
|
|
17
|
-
formatSystemTimestamp,
|
|
18
|
-
isListOperator,
|
|
19
|
-
isSortableField,
|
|
20
|
-
isValuelessOperator,
|
|
21
|
-
operatorsForField,
|
|
22
|
-
RESERVED_FIELD_NAMES,
|
|
23
|
-
SYSTEM_FIELD_OPERATORS,
|
|
24
|
-
} from "../recordValues.js";
|
|
25
|
-
|
|
26
|
-
function field(overrides: {
|
|
27
|
-
name?: string;
|
|
28
|
-
type: FieldType;
|
|
29
|
-
required?: boolean;
|
|
30
|
-
enumValues?: string[];
|
|
31
|
-
}) {
|
|
32
|
-
return create(FieldDeclarationSchema, {
|
|
33
|
-
name: overrides.name ?? "f",
|
|
34
|
-
type: overrides.type,
|
|
35
|
-
required: overrides.required ?? false,
|
|
36
|
-
enumValues: overrides.enumValues ?? [],
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("coerceFieldValue", () => {
|
|
41
|
-
it("passes explicit null through for every type (tri-state clear)", () => {
|
|
42
|
-
for (const type of [
|
|
43
|
-
FieldType.string,
|
|
44
|
-
FieldType.integer,
|
|
45
|
-
FieldType.number,
|
|
46
|
-
FieldType.bool,
|
|
47
|
-
FieldType.timestamp,
|
|
48
|
-
FieldType.date,
|
|
49
|
-
FieldType.time,
|
|
50
|
-
FieldType.json,
|
|
51
|
-
]) {
|
|
52
|
-
expect(coerceFieldValue(field({ type }), null)).toEqual({ ok: true, value: null });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("string", () => {
|
|
57
|
-
it("accepts a plain string", () => {
|
|
58
|
-
expect(coerceFieldValue(field({ type: FieldType.string }), "hello")).toEqual({
|
|
59
|
-
ok: true,
|
|
60
|
-
value: "hello",
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("rejects non-strings", () => {
|
|
65
|
-
const r = coerceFieldValue(field({ name: "note", type: FieldType.string }), 5);
|
|
66
|
-
expect(r).toEqual({ ok: false, error: 'field "note" must be a string' });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("enforces enum_values membership", () => {
|
|
70
|
-
const f = field({
|
|
71
|
-
name: "status",
|
|
72
|
-
type: FieldType.string,
|
|
73
|
-
enumValues: ["confirmed", "cancelled"],
|
|
74
|
-
});
|
|
75
|
-
expect(coerceFieldValue(f, "confirmed")).toEqual({ ok: true, value: "confirmed" });
|
|
76
|
-
expect(coerceFieldValue(f, "pending")).toEqual({
|
|
77
|
-
ok: false,
|
|
78
|
-
error: 'field "status" must be one of [confirmed cancelled]',
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("integer", () => {
|
|
84
|
-
it("accepts integral numbers", () => {
|
|
85
|
-
expect(coerceFieldValue(field({ type: FieldType.integer }), 42)).toEqual({
|
|
86
|
-
ok: true,
|
|
87
|
-
value: 42,
|
|
88
|
-
});
|
|
89
|
-
expect(coerceFieldValue(field({ type: FieldType.integer }), -7)).toEqual({
|
|
90
|
-
ok: true,
|
|
91
|
-
value: -7,
|
|
92
|
-
});
|
|
93
|
-
expect(coerceFieldValue(field({ type: FieldType.integer }), 0)).toEqual({
|
|
94
|
-
ok: true,
|
|
95
|
-
value: 0,
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("rejects fractional, non-numeric, and unsafe values", () => {
|
|
100
|
-
const f = field({ name: "n", type: FieldType.integer });
|
|
101
|
-
expect(coerceFieldValue(f, 1.5)).toMatchObject({ ok: false });
|
|
102
|
-
expect(coerceFieldValue(f, "3")).toMatchObject({ ok: false });
|
|
103
|
-
expect(coerceFieldValue(f, Number.MAX_SAFE_INTEGER + 2)).toMatchObject({ ok: false });
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("number", () => {
|
|
108
|
-
it("accepts finite numbers", () => {
|
|
109
|
-
expect(coerceFieldValue(field({ type: FieldType.number }), 3.14)).toEqual({
|
|
110
|
-
ok: true,
|
|
111
|
-
value: 3.14,
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("rejects non-numbers and non-finite values", () => {
|
|
116
|
-
const f = field({ name: "n", type: FieldType.number });
|
|
117
|
-
expect(coerceFieldValue(f, "3.14")).toMatchObject({ ok: false });
|
|
118
|
-
expect(coerceFieldValue(f, Number.POSITIVE_INFINITY)).toMatchObject({ ok: false });
|
|
119
|
-
expect(coerceFieldValue(f, Number.NaN)).toMatchObject({ ok: false });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("bool", () => {
|
|
124
|
-
it("accepts booleans and rejects everything else", () => {
|
|
125
|
-
const f = field({ name: "b", type: FieldType.bool });
|
|
126
|
-
expect(coerceFieldValue(f, true)).toEqual({ ok: true, value: true });
|
|
127
|
-
expect(coerceFieldValue(f, false)).toEqual({ ok: true, value: false });
|
|
128
|
-
expect(coerceFieldValue(f, "true")).toMatchObject({ ok: false });
|
|
129
|
-
expect(coerceFieldValue(f, 1)).toMatchObject({ ok: false });
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("timestamp", () => {
|
|
134
|
-
const f = field({ name: "at", type: FieldType.timestamp });
|
|
135
|
-
|
|
136
|
-
it("accepts RFC 3339 UTC and offset forms", () => {
|
|
137
|
-
expect(coerceFieldValue(f, "2026-07-21T04:30:00Z")).toEqual({
|
|
138
|
-
ok: true,
|
|
139
|
-
value: "2026-07-21T04:30:00Z",
|
|
140
|
-
});
|
|
141
|
-
expect(coerceFieldValue(f, "2026-07-21T04:30:00.123Z")).toMatchObject({ ok: true });
|
|
142
|
-
expect(coerceFieldValue(f, "2026-07-21T10:00:00+05:30")).toMatchObject({ ok: true });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("rejects date-only, missing zone, and garbage", () => {
|
|
146
|
-
expect(coerceFieldValue(f, "2026-07-21")).toMatchObject({ ok: false });
|
|
147
|
-
expect(coerceFieldValue(f, "2026-07-21T04:30:00")).toMatchObject({ ok: false });
|
|
148
|
-
expect(coerceFieldValue(f, "yesterday")).toMatchObject({ ok: false });
|
|
149
|
-
expect(coerceFieldValue(f, 1721536200)).toMatchObject({ ok: false });
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("date", () => {
|
|
154
|
-
const f = field({ name: "day", type: FieldType.date });
|
|
155
|
-
|
|
156
|
-
it("accepts valid YYYY-MM-DD", () => {
|
|
157
|
-
expect(coerceFieldValue(f, "2026-07-21")).toEqual({ ok: true, value: "2026-07-21" });
|
|
158
|
-
expect(coerceFieldValue(f, "2024-02-29")).toMatchObject({ ok: true }); // leap day
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("rejects impossible dates and other shapes", () => {
|
|
162
|
-
expect(coerceFieldValue(f, "2026-02-30")).toMatchObject({ ok: false });
|
|
163
|
-
expect(coerceFieldValue(f, "2026-13-01")).toMatchObject({ ok: false });
|
|
164
|
-
expect(coerceFieldValue(f, "2023-02-29")).toMatchObject({ ok: false }); // not a leap year
|
|
165
|
-
expect(coerceFieldValue(f, "21-07-2026")).toMatchObject({ ok: false });
|
|
166
|
-
expect(coerceFieldValue(f, "2026-7-1")).toMatchObject({ ok: false }); // unpadded
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("time", () => {
|
|
171
|
-
const f = field({ name: "slot", type: FieldType.time });
|
|
172
|
-
|
|
173
|
-
it("canonicalizes HH:MM to HH:MM:SS", () => {
|
|
174
|
-
expect(coerceFieldValue(f, "09:30")).toEqual({ ok: true, value: "09:30:00" });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("passes HH:MM:SS through", () => {
|
|
178
|
-
expect(coerceFieldValue(f, "23:59:59")).toEqual({ ok: true, value: "23:59:59" });
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("rejects unpadded, out-of-range, and garbage input", () => {
|
|
182
|
-
expect(coerceFieldValue(f, "9:30")).toMatchObject({ ok: false }); // unpadded
|
|
183
|
-
expect(coerceFieldValue(f, "24:00")).toMatchObject({ ok: false });
|
|
184
|
-
expect(coerceFieldValue(f, "12:60")).toMatchObject({ ok: false });
|
|
185
|
-
expect(coerceFieldValue(f, "12:00:60")).toMatchObject({ ok: false });
|
|
186
|
-
expect(coerceFieldValue(f, "noon")).toMatchObject({ ok: false });
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("json", () => {
|
|
191
|
-
it("passes any JSON value through untouched", () => {
|
|
192
|
-
const f = field({ type: FieldType.json });
|
|
193
|
-
const nested = { a: [1, "two", { three: true }] };
|
|
194
|
-
expect(coerceFieldValue(f, nested)).toEqual({ ok: true, value: nested });
|
|
195
|
-
expect(coerceFieldValue(f, [1, 2])).toMatchObject({ ok: true });
|
|
196
|
-
expect(coerceFieldValue(f, "plain")).toMatchObject({ ok: true });
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("rejects unspecified field types", () => {
|
|
201
|
-
expect(
|
|
202
|
-
coerceFieldValue(field({ type: FieldType.field_type_unspecified }), "x"),
|
|
203
|
-
).toMatchObject({ ok: false });
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("formatFieldValue", () => {
|
|
208
|
-
it("renders null/undefined as empty", () => {
|
|
209
|
-
expect(formatFieldValue(FieldType.string, null)).toBe("");
|
|
210
|
-
expect(formatFieldValue(FieldType.integer, undefined)).toBe("");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("trims the stored timestamp's zero fraction", () => {
|
|
214
|
-
expect(formatFieldValue(FieldType.timestamp, "2026-07-21T04:30:00.000000000Z")).toBe(
|
|
215
|
-
"2026-07-21T04:30:00Z",
|
|
216
|
-
);
|
|
217
|
-
// Non-zero fractions carry information and stay.
|
|
218
|
-
expect(formatFieldValue(FieldType.timestamp, "2026-07-21T04:30:00.500000000Z")).toBe(
|
|
219
|
-
"2026-07-21T04:30:00.500000000Z",
|
|
220
|
-
);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("stringifies json values compactly", () => {
|
|
224
|
-
expect(formatFieldValue(FieldType.json, { a: 1 })).toBe('{"a":1}');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("renders scalars canonically", () => {
|
|
228
|
-
expect(formatFieldValue(FieldType.bool, true)).toBe("true");
|
|
229
|
-
expect(formatFieldValue(FieldType.integer, 42)).toBe("42");
|
|
230
|
-
expect(formatFieldValue(FieldType.date, "2026-07-21")).toBe("2026-07-21");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("formats system timestamps the same way", () => {
|
|
234
|
-
expect(formatSystemTimestamp("2026-07-21T04:30:00.000000000Z")).toBe(
|
|
235
|
-
"2026-07-21T04:30:00Z",
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe("operator matrix", () => {
|
|
241
|
-
it("string fields get equality operators", () => {
|
|
242
|
-
expect(operatorsForField(field({ type: FieldType.string, required: true }))).toEqual([
|
|
243
|
-
RecordConditionOp.eq,
|
|
244
|
-
RecordConditionOp.neq,
|
|
245
|
-
RecordConditionOp.is_in,
|
|
246
|
-
RecordConditionOp.not_in,
|
|
247
|
-
]);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("bool fields get eq/neq only", () => {
|
|
251
|
-
expect(operatorsForField(field({ type: FieldType.bool, required: true }))).toEqual([
|
|
252
|
-
RecordConditionOp.eq,
|
|
253
|
-
RecordConditionOp.neq,
|
|
254
|
-
]);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("numeric and temporal fields get range operators", () => {
|
|
258
|
-
for (const type of [
|
|
259
|
-
FieldType.integer,
|
|
260
|
-
FieldType.number,
|
|
261
|
-
FieldType.timestamp,
|
|
262
|
-
FieldType.date,
|
|
263
|
-
FieldType.time,
|
|
264
|
-
]) {
|
|
265
|
-
expect(operatorsForField(field({ type, required: true }))).toEqual([
|
|
266
|
-
RecordConditionOp.eq,
|
|
267
|
-
RecordConditionOp.neq,
|
|
268
|
-
RecordConditionOp.gt,
|
|
269
|
-
RecordConditionOp.gte,
|
|
270
|
-
RecordConditionOp.lt,
|
|
271
|
-
RecordConditionOp.lte,
|
|
272
|
-
]);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("json fields are not filterable — even optional ones get no null tests", () => {
|
|
277
|
-
expect(operatorsForField(field({ type: FieldType.json }))).toEqual([]);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("optional fields additionally get null tests", () => {
|
|
281
|
-
const ops = operatorsForField(field({ type: FieldType.string, required: false }));
|
|
282
|
-
expect(ops).toContain(RecordConditionOp.is_null);
|
|
283
|
-
expect(ops).toContain(RecordConditionOp.not_null);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("required fields never get null tests", () => {
|
|
287
|
-
const ops = operatorsForField(field({ type: FieldType.string, required: true }));
|
|
288
|
-
expect(ops).not.toContain(RecordConditionOp.is_null);
|
|
289
|
-
expect(ops).not.toContain(RecordConditionOp.not_null);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("system fields mirror the server matrix", () => {
|
|
293
|
-
expect(SYSTEM_FIELD_OPERATORS.get("id")).toEqual([
|
|
294
|
-
RecordConditionOp.eq,
|
|
295
|
-
RecordConditionOp.is_in,
|
|
296
|
-
]);
|
|
297
|
-
expect(SYSTEM_FIELD_OPERATORS.get("created_at")).toEqual([
|
|
298
|
-
RecordConditionOp.gt,
|
|
299
|
-
RecordConditionOp.gte,
|
|
300
|
-
RecordConditionOp.lt,
|
|
301
|
-
RecordConditionOp.lte,
|
|
302
|
-
]);
|
|
303
|
-
expect(SYSTEM_FIELD_OPERATORS.get("updated_at")).toEqual(
|
|
304
|
-
SYSTEM_FIELD_OPERATORS.get("created_at"),
|
|
305
|
-
);
|
|
306
|
-
// Not filterable: attribution and ambient scope.
|
|
307
|
-
expect(SYSTEM_FIELD_OPERATORS.has("created_by")).toBe(false);
|
|
308
|
-
expect(SYSTEM_FIELD_OPERATORS.has("org")).toBe(false);
|
|
309
|
-
expect(SYSTEM_FIELD_OPERATORS.has("partition")).toBe(false);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("classifies operator arity", () => {
|
|
313
|
-
expect(isValuelessOperator(RecordConditionOp.is_null)).toBe(true);
|
|
314
|
-
expect(isValuelessOperator(RecordConditionOp.not_null)).toBe(true);
|
|
315
|
-
expect(isValuelessOperator(RecordConditionOp.eq)).toBe(false);
|
|
316
|
-
expect(isListOperator(RecordConditionOp.is_in)).toBe(true);
|
|
317
|
-
expect(isListOperator(RecordConditionOp.not_in)).toBe(true);
|
|
318
|
-
expect(isListOperator(RecordConditionOp.eq)).toBe(false);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe("isSortableField", () => {
|
|
323
|
-
it("declared non-json fields sort; json does not", () => {
|
|
324
|
-
expect(isSortableField(field({ type: FieldType.date }))).toBe(true);
|
|
325
|
-
expect(isSortableField(field({ type: FieldType.string }))).toBe(true);
|
|
326
|
-
expect(isSortableField(field({ type: FieldType.json }))).toBe(false);
|
|
327
|
-
expect(isSortableField(field({ type: FieldType.field_type_unspecified }))).toBe(false);
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
describe("buildUpdateFields", () => {
|
|
332
|
-
it("carries dirty values and explicit nulls, omits untouched fields", () => {
|
|
333
|
-
const payload = buildUpdateFields(
|
|
334
|
-
new Map<string, string | null>([
|
|
335
|
-
["patient_phone", "+15550100"],
|
|
336
|
-
["notes", null],
|
|
337
|
-
]),
|
|
338
|
-
);
|
|
339
|
-
expect(payload).toEqual({ patient_phone: "+15550100", notes: null });
|
|
340
|
-
expect("slot_time" in payload).toBe(false);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("produces an empty object for no edits", () => {
|
|
344
|
-
expect(buildUpdateFields(new Map())).toEqual({});
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
describe("RESERVED_FIELD_NAMES", () => {
|
|
349
|
-
it("matches the server's reserved set", () => {
|
|
350
|
-
expect([...RESERVED_FIELD_NAMES].sort()).toEqual([
|
|
351
|
-
"created_at",
|
|
352
|
-
"created_by",
|
|
353
|
-
"id",
|
|
354
|
-
"org",
|
|
355
|
-
"partition",
|
|
356
|
-
"updated_at",
|
|
357
|
-
]);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// Render tests for the two Overview views: the schema view (spec for
|
|
2
|
-
// structure — fields, constraints with authored messages, authorization
|
|
3
|
-
// summary) and the sync report (status for health).
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
6
|
-
import { render, screen, cleanup } from "@testing-library/react";
|
|
7
|
-
import { create } from "@bufbuild/protobuf";
|
|
8
|
-
import {
|
|
9
|
-
DatastoreSpecSchema,
|
|
10
|
-
FieldType,
|
|
11
|
-
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/spec_pb";
|
|
12
|
-
import {
|
|
13
|
-
CollectionMaterializationState,
|
|
14
|
-
DatastoreStatusSchema,
|
|
15
|
-
DatastoreSyncOutcome,
|
|
16
|
-
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/status_pb";
|
|
17
|
-
import { CollectionSchemaView, formatSubject } from "../CollectionSchemaView";
|
|
18
|
-
import { DatastoreSyncReport } from "../DatastoreSyncReport";
|
|
19
|
-
|
|
20
|
-
afterEach(() => cleanup());
|
|
21
|
-
|
|
22
|
-
describe("CollectionSchemaView", () => {
|
|
23
|
-
const spec = create(DatastoreSpecSchema, {
|
|
24
|
-
timezone: "Asia/Kolkata",
|
|
25
|
-
authorization: {
|
|
26
|
-
roles: [{ name: "admin" }, { name: "patient" }],
|
|
27
|
-
defaultRole: "patient",
|
|
28
|
-
bindings: [
|
|
29
|
-
{
|
|
30
|
-
subject: {
|
|
31
|
-
kind: { case: "channelSender", value: { senderKind: "whatsapp", value: "+15550100" } },
|
|
32
|
-
},
|
|
33
|
-
role: "admin",
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
collections: [
|
|
38
|
-
{
|
|
39
|
-
name: "bookings",
|
|
40
|
-
description: "Confirmed appointments",
|
|
41
|
-
fields: [
|
|
42
|
-
{ name: "slot_date", type: FieldType.date, required: true },
|
|
43
|
-
{
|
|
44
|
-
name: "status",
|
|
45
|
-
type: FieldType.string,
|
|
46
|
-
enumValues: ["confirmed", "cancelled"],
|
|
47
|
-
description: "Booking lifecycle",
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
uniques: [
|
|
51
|
-
{
|
|
52
|
-
name: "one_confirmed_per_slot",
|
|
53
|
-
fields: ["slot_date"],
|
|
54
|
-
message: "that slot is already booked",
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("renders fields, enum values, and required markers", () => {
|
|
62
|
-
render(<CollectionSchemaView spec={spec} />);
|
|
63
|
-
expect(screen.getByText("slot_date")).toBeTruthy();
|
|
64
|
-
expect(screen.getByText(/confirmed \| cancelled/)).toBeTruthy();
|
|
65
|
-
expect(screen.getByText("Booking lifecycle")).toBeTruthy();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("renders constraints with the operator's authored message verbatim", () => {
|
|
69
|
-
render(<CollectionSchemaView spec={spec} />);
|
|
70
|
-
expect(screen.getByText("one_confirmed_per_slot")).toBeTruthy();
|
|
71
|
-
expect(screen.getByText(/that slot is already booked/)).toBeTruthy();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("summarizes the authorization block: roles, bindings, default_role, timezone", () => {
|
|
75
|
-
render(<CollectionSchemaView spec={spec} />);
|
|
76
|
-
// "admin" appears as a role chip and as the binding's role.
|
|
77
|
-
expect(screen.getAllByText("admin").length).toBeGreaterThan(0);
|
|
78
|
-
// "patient" appears as a role chip and as the default_role chip.
|
|
79
|
-
expect(screen.getAllByText("patient").length).toBeGreaterThan(0);
|
|
80
|
-
expect(screen.getByText("whatsapp:+15550100")).toBeTruthy();
|
|
81
|
-
expect(screen.getByText("Asia/Kolkata")).toBeTruthy();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("states the deny-by-default posture when no default_role is set", () => {
|
|
85
|
-
const noDefault = create(DatastoreSpecSchema, {
|
|
86
|
-
authorization: { roles: [{ name: "admin" }] },
|
|
87
|
-
collections: [{ name: "c", fields: [{ name: "f", type: FieldType.string }] }],
|
|
88
|
-
});
|
|
89
|
-
render(<CollectionSchemaView spec={noDefault} />);
|
|
90
|
-
expect(screen.getByText(/unbound callers are denied/)).toBeTruthy();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("formatSubject", () => {
|
|
95
|
-
it("renders channel senders and principals plainly", () => {
|
|
96
|
-
expect(
|
|
97
|
-
formatSubject({
|
|
98
|
-
kind: { case: "channelSender", value: { senderKind: "whatsapp", value: "+1" } },
|
|
99
|
-
} as never),
|
|
100
|
-
).toBe("whatsapp:+1");
|
|
101
|
-
expect(
|
|
102
|
-
formatSubject({
|
|
103
|
-
kind: { case: "principal", value: { kind: "identity_account", id: "ida_1" } },
|
|
104
|
-
} as never),
|
|
105
|
-
).toBe("identity_account/ida_1");
|
|
106
|
-
expect(formatSubject(undefined)).toBe("unknown");
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("DatastoreSyncReport", () => {
|
|
111
|
-
it("renders the outcome badge and per-collection counts", () => {
|
|
112
|
-
const status = create(DatastoreStatusSchema, {
|
|
113
|
-
lastSyncOutcome: DatastoreSyncOutcome.synced,
|
|
114
|
-
collections: [
|
|
115
|
-
{
|
|
116
|
-
name: "bookings",
|
|
117
|
-
state: CollectionMaterializationState.active,
|
|
118
|
-
recordCount: 214n,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "old_notes",
|
|
122
|
-
state: CollectionMaterializationState.removed,
|
|
123
|
-
recordCount: 3n,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
});
|
|
127
|
-
render(<DatastoreSyncReport status={status} />);
|
|
128
|
-
expect(screen.getByText("synced")).toBeTruthy();
|
|
129
|
-
expect(screen.getByText("214")).toBeTruthy();
|
|
130
|
-
expect(screen.getByText("active")).toBeTruthy();
|
|
131
|
-
expect(screen.getByText("removed")).toBeTruthy();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("renders the rejected outcome", () => {
|
|
135
|
-
const status = create(DatastoreStatusSchema, {
|
|
136
|
-
lastSyncOutcome: DatastoreSyncOutcome.rejected,
|
|
137
|
-
});
|
|
138
|
-
render(<DatastoreSyncReport status={status} />);
|
|
139
|
-
expect(screen.getByText("rejected")).toBeTruthy();
|
|
140
|
-
expect(screen.getByText(/No collections have been synced yet/)).toBeTruthy();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import { create } from "@bufbuild/protobuf";
|
|
5
|
-
import { DatastoreDescriptionSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_io_pb";
|
|
6
|
-
import { StigmerContext } from "../../context";
|
|
7
|
-
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
8
|
-
import { useDatastoreDescription } from "../useDatastoreDescription";
|
|
9
|
-
|
|
10
|
-
function createMockStigmer(overrides: {
|
|
11
|
-
describeDatastore?: (...args: unknown[]) => Promise<unknown>;
|
|
12
|
-
} = {}) {
|
|
13
|
-
return {
|
|
14
|
-
datastore: {
|
|
15
|
-
describeDatastore:
|
|
16
|
-
overrides.describeDatastore ?? vi.fn().mockResolvedValue(null),
|
|
17
|
-
},
|
|
18
|
-
} as never;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function wrapper(client: unknown) {
|
|
22
|
-
return function Wrapper({ children }: { children: ReactNode }) {
|
|
23
|
-
return (
|
|
24
|
-
<FetchCacheContext.Provider value={null}>
|
|
25
|
-
<StigmerContext.Provider value={client as never}>
|
|
26
|
-
{children}
|
|
27
|
-
</StigmerContext.Provider>
|
|
28
|
-
</FetchCacheContext.Provider>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe("useDatastoreDescription", () => {
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
vi.restoreAllMocks();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("fetches the description and passes org + datastore through", async () => {
|
|
39
|
-
const description = create(DatastoreDescriptionSchema, {
|
|
40
|
-
datastore: "clinic",
|
|
41
|
-
partitions: ["default", "dr-alt"],
|
|
42
|
-
});
|
|
43
|
-
const describeDatastore = vi.fn().mockResolvedValue(description);
|
|
44
|
-
const client = createMockStigmer({ describeDatastore });
|
|
45
|
-
|
|
46
|
-
const { result } = renderHook(() => useDatastoreDescription("acme", "clinic"), {
|
|
47
|
-
wrapper: wrapper(client),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
51
|
-
|
|
52
|
-
expect(result.current.description).toBe(description);
|
|
53
|
-
expect(describeDatastore).toHaveBeenCalledWith(
|
|
54
|
-
expect.objectContaining({ org: "acme", datastore: "clinic" }),
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("always offers the default partition even when the catalog is empty", async () => {
|
|
59
|
-
// The catalog materializes with the first record write; an empty
|
|
60
|
-
// datastore reports no partitions but "default" is always addressable.
|
|
61
|
-
const description = create(DatastoreDescriptionSchema, {
|
|
62
|
-
datastore: "clinic",
|
|
63
|
-
partitions: [],
|
|
64
|
-
});
|
|
65
|
-
const client = createMockStigmer({
|
|
66
|
-
describeDatastore: vi.fn().mockResolvedValue(description),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const { result } = renderHook(() => useDatastoreDescription("acme", "clinic"), {
|
|
70
|
-
wrapper: wrapper(client),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
74
|
-
expect(result.current.partitions).toEqual(["default"]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("prepends default when the catalog holds only named partitions", async () => {
|
|
78
|
-
const description = create(DatastoreDescriptionSchema, {
|
|
79
|
-
datastore: "clinic",
|
|
80
|
-
partitions: ["dr-alt"],
|
|
81
|
-
});
|
|
82
|
-
const client = createMockStigmer({
|
|
83
|
-
describeDatastore: vi.fn().mockResolvedValue(description),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const { result } = renderHook(() => useDatastoreDescription("acme", "clinic"), {
|
|
87
|
-
wrapper: wrapper(client),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
91
|
-
expect(result.current.partitions).toEqual(["default", "dr-alt"]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("does not duplicate default when the catalog already carries it", async () => {
|
|
95
|
-
const description = create(DatastoreDescriptionSchema, {
|
|
96
|
-
datastore: "clinic",
|
|
97
|
-
partitions: ["default", "dr-alt"],
|
|
98
|
-
});
|
|
99
|
-
const client = createMockStigmer({
|
|
100
|
-
describeDatastore: vi.fn().mockResolvedValue(description),
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const { result } = renderHook(() => useDatastoreDescription("acme", "clinic"), {
|
|
104
|
-
wrapper: wrapper(client),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
108
|
-
expect(result.current.partitions).toEqual(["default", "dr-alt"]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("skips fetching when org or datastore is null", () => {
|
|
112
|
-
const describeDatastore = vi.fn();
|
|
113
|
-
const client = createMockStigmer({ describeDatastore });
|
|
114
|
-
|
|
115
|
-
const { result } = renderHook(() => useDatastoreDescription(null, "clinic"), {
|
|
116
|
-
wrapper: wrapper(client),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
expect(result.current.isLoading).toBe(false);
|
|
120
|
-
expect(result.current.description).toBeNull();
|
|
121
|
-
expect(result.current.partitions).toEqual(["default"]);
|
|
122
|
-
expect(describeDatastore).not.toHaveBeenCalled();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("surfaces PERMISSION_DENIED as error — the defensive denied-state branch", async () => {
|
|
126
|
-
// A reach-level denial must be catchable and rendered as the
|
|
127
|
-
// Records-tab denied panel, never a page-level failure.
|
|
128
|
-
const denial = new Error("unauthorized to use records in this datastore");
|
|
129
|
-
const client = createMockStigmer({
|
|
130
|
-
describeDatastore: vi.fn().mockRejectedValue(denial),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const { result } = renderHook(() => useDatastoreDescription("acme", "clinic"), {
|
|
134
|
-
wrapper: wrapper(client),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
138
|
-
expect(result.current.error?.message).toBe(
|
|
139
|
-
"unauthorized to use records in this datastore",
|
|
140
|
-
);
|
|
141
|
-
expect(result.current.description).toBeNull();
|
|
142
|
-
});
|
|
143
|
-
});
|