primitive-admin 1.2.0-alpha.0 → 1.2.0-alpha.1
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/README.md +3 -2
- package/dist/src/commands/connections.js +1 -5
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/databases.js +7 -109
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/sessions.js +3 -8
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync.d.ts +76 -3
- package/dist/src/commands/sync.js +326 -153
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +1 -6
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +0 -30
- package/dist/src/lib/api-client.js +0 -37
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +2 -3
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +4 -2
- package/dist/src/lib/generated-config-surfaces.js +82 -36
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/package.json +2 -2
|
@@ -1629,8 +1629,8 @@ export function serializeBlobBucket(bucket, logger) {
|
|
|
1629
1629
|
*
|
|
1630
1630
|
* EXACT extraction of the inline builder shared by the existing-update and
|
|
1631
1631
|
* 409-adopt branches — do NOT "clean up" the truthiness checks on the access
|
|
1632
|
-
* model. The server treats `preset` and `
|
|
1633
|
-
* only clears `ruleSetId` when a preset
|
|
1632
|
+
* model. The server treats `preset` and `ruleSetId` as mutually exclusive,
|
|
1633
|
+
* only clears `ruleSetId` when a preset is also present, and
|
|
1634
1634
|
* refuses to leave a bucket with no access model or a blank name — so those
|
|
1635
1635
|
* fields are not clearable. `bucketKey` and `ttlTier` are immutable and never
|
|
1636
1636
|
* sent. Both call sites must go through this helper so their field sets never
|
|
@@ -1655,12 +1655,6 @@ export function buildBlobBucketUpdatePayload(bucket) {
|
|
|
1655
1655
|
description: tomlOwned,
|
|
1656
1656
|
},
|
|
1657
1657
|
});
|
|
1658
|
-
// `accessPolicy` is the pre-#1020 spelling of `preset`, declared in the
|
|
1659
|
-
// definition's `tomlOnlyKeys`: an unmigrated file still pushes its access
|
|
1660
|
-
// model, and the server takes at most one of the two.
|
|
1661
|
-
if (!updatePayload.preset && bucket?.accessPolicy) {
|
|
1662
|
-
updatePayload.accessPolicy = bucket.accessPolicy;
|
|
1663
|
-
}
|
|
1664
1658
|
for (const key of Object.keys(updatePayload)) {
|
|
1665
1659
|
if (updatePayload[key] === undefined)
|
|
1666
1660
|
delete updatePayload[key];
|
|
@@ -1761,9 +1755,6 @@ export function buildBlobBucketCreatePayload(key, bucket) {
|
|
|
1761
1755
|
},
|
|
1762
1756
|
}),
|
|
1763
1757
|
};
|
|
1764
|
-
if (!payload.preset && bucket?.accessPolicy) {
|
|
1765
|
-
payload.accessPolicy = bucket.accessPolicy;
|
|
1766
|
-
}
|
|
1767
1758
|
for (const field of Object.keys(payload)) {
|
|
1768
1759
|
if (payload[field] === undefined)
|
|
1769
1760
|
delete payload[field];
|
|
@@ -4535,16 +4526,16 @@ function jsonFieldsForDiff(table, entity) {
|
|
|
4535
4526
|
/**
|
|
4536
4527
|
* One test-case reference as the comparison reads it (#2880 behavior 17).
|
|
4537
4528
|
*
|
|
4538
|
-
* A reference reaches the projection in
|
|
4539
|
-
* describe the same thing on both sides once this is
|
|
4529
|
+
* A reference reaches the projection as the value in its NAME slot, in one of
|
|
4530
|
+
* two states, and both describe the same thing on both sides once this is
|
|
4531
|
+
* applied:
|
|
4540
4532
|
*
|
|
4541
|
-
* - a portable NAME (`configName = "default"`)
|
|
4542
|
-
*
|
|
4543
|
-
* - a
|
|
4544
|
-
*
|
|
4545
|
-
*
|
|
4546
|
-
*
|
|
4547
|
-
* the next push still sends.
|
|
4533
|
+
* - a portable NAME (`configName = "default"`) — what a test file carries,
|
|
4534
|
+
* and what the server's id renders as when the lookups resolve it;
|
|
4535
|
+
* - a live ID the lookups could NOT resolve, which the comparison's rendering
|
|
4536
|
+
* (`serializeTestCaseForComparison`) carries in the name slot and which
|
|
4537
|
+
* stays the id here, so a case pinned to a deleted or unlistable config is
|
|
4538
|
+
* a difference against an unpinned file rather than hidden (#3998).
|
|
4548
4539
|
*/
|
|
4549
4540
|
function testCaseReference(value, idToName) {
|
|
4550
4541
|
if (typeof value !== "string" || value === "")
|
|
@@ -4821,14 +4812,6 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4821
4812
|
table: BLOB_BUCKET_TABLE,
|
|
4822
4813
|
parse: (doc) => {
|
|
4823
4814
|
const bucket = { ...(doc?.bucket ?? {}) };
|
|
4824
|
-
// #1020 — `accessPolicy` is the pre-`preset` spelling, still accepted on
|
|
4825
|
-
// the wire and still authored in unmigrated files. Both spellings mean
|
|
4826
|
-
// the same access model, so they project the same way; without this an
|
|
4827
|
-
// unmigrated file reports a difference no push can clear (push sends the
|
|
4828
|
-
// legacy key and the server answers with `preset`).
|
|
4829
|
-
if (bucket.preset === undefined && bucket.accessPolicy !== undefined) {
|
|
4830
|
-
bucket.preset = bucket.accessPolicy;
|
|
4831
|
-
}
|
|
4832
4815
|
return {
|
|
4833
4816
|
bucketKey: doc?.bucket?.key ?? "",
|
|
4834
4817
|
...buildPayloadFromToml(BLOB_BUCKET_TABLE, bucket, {
|
|
@@ -4862,7 +4845,7 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4862
4845
|
const keys = [];
|
|
4863
4846
|
if (!bucket.name)
|
|
4864
4847
|
keys.push("name");
|
|
4865
|
-
if (!bucket.preset
|
|
4848
|
+
if (!bucket.preset)
|
|
4866
4849
|
keys.push("preset");
|
|
4867
4850
|
if (!bucket.ruleSetId)
|
|
4868
4851
|
keys.push("ruleSetId");
|
|
@@ -4874,17 +4857,15 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4874
4857
|
// edited expectation read as Synced while push applied it from the file's
|
|
4875
4858
|
// byte hash.
|
|
4876
4859
|
//
|
|
4877
|
-
// The type's own difficulty is that a reference
|
|
4878
|
-
//
|
|
4879
|
-
//
|
|
4880
|
-
//
|
|
4881
|
-
// `extra` context is what makes that possible (the id→name lookups are not in
|
|
4882
|
-
// the document).
|
|
4860
|
+
// The type's own difficulty is that a reference is a NAME in the file and an
|
|
4861
|
+
// id on the server. The comparison renders the server's id through the
|
|
4862
|
+
// id→name lookups (which are not in the document — the `extra` context
|
|
4863
|
+
// carries them), so both sides project the thing referred to.
|
|
4883
4864
|
"test-case": {
|
|
4884
4865
|
label: "test-case",
|
|
4885
4866
|
table: TEST_CASE_TABLE,
|
|
4886
4867
|
parse: (doc) => parseTestCaseToml(doc),
|
|
4887
|
-
serialize: (record, _maps, extra) =>
|
|
4868
|
+
serialize: (record, _maps, extra) => serializeTestCaseForComparison(record, extra?.lookupMaps),
|
|
4888
4869
|
// The server trims a test-case name on write (`src/admin-api.ts`), so a
|
|
4889
4870
|
// padded one in the file is not a difference — it is what the server will
|
|
4890
4871
|
// store (#2880 DSO-003).
|
|
@@ -4894,12 +4875,11 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4894
4875
|
},
|
|
4895
4876
|
extras: (entity, _doc, extra) => ({
|
|
4896
4877
|
// These three OVERRIDE the definition's fields of the same name: the
|
|
4897
|
-
// value compared is the thing referred to,
|
|
4898
|
-
//
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
evaluatorConfigId: testCaseReference(entity.evaluatorConfigId ?? entity._evaluatorConfigName, extra?.lookupMaps?.configIdToName),
|
|
4878
|
+
// value compared is the thing referred to, read from the name the file
|
|
4879
|
+
// (or the comparison's rendering of the server record) carries.
|
|
4880
|
+
configId: testCaseReference(entity._configName, extra?.lookupMaps?.configIdToName),
|
|
4881
|
+
evaluatorPromptId: testCaseReference(entity._evaluatorPromptKey, extra?.lookupMaps?.promptIdToKey),
|
|
4882
|
+
evaluatorConfigId: testCaseReference(entity._evaluatorConfigName, extra?.lookupMaps?.configIdToName),
|
|
4903
4883
|
}),
|
|
4904
4884
|
describe: (entity) => `test case "${entity?.name}"`,
|
|
4905
4885
|
},
|
|
@@ -5718,49 +5698,81 @@ export function getTestsDir(configDir, blockType, blockKey) {
|
|
|
5718
5698
|
function blockSelectionLabel(blockType) {
|
|
5719
5699
|
return blockType === "script" ? "transform" : blockType;
|
|
5720
5700
|
}
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5701
|
+
/**
|
|
5702
|
+
* The `[test]` table for one server record, and the references it could not
|
|
5703
|
+
* name (#3998).
|
|
5704
|
+
*
|
|
5705
|
+
* A sidecar authors its references by NAME only — the id spellings are retired
|
|
5706
|
+
* — so a reference whose id has no lookup entry has no honest spelling in a
|
|
5707
|
+
* file. `unresolvedSpelling` says what goes in the name slot for one: `"empty"`
|
|
5708
|
+
* for the file pull writes (and the caller decides whether to write it at all),
|
|
5709
|
+
* `"raw-id"` for the comparison's rendering, which is parsed and never written.
|
|
5710
|
+
*/
|
|
5711
|
+
function testCaseSidecarTable(testCase, lookupMaps, unresolvedSpelling) {
|
|
5712
|
+
const unresolved = [];
|
|
5713
|
+
const reference = (field, tomlKey, idToName) => {
|
|
5714
|
+
const id = testCase?.[field];
|
|
5715
|
+
if (typeof id !== "string" || id === "")
|
|
5716
|
+
return "";
|
|
5717
|
+
const name = idToName?.get(id);
|
|
5718
|
+
if (name)
|
|
5719
|
+
return name;
|
|
5720
|
+
unresolved.push({ tomlKey, field, id });
|
|
5721
|
+
return unresolvedSpelling === "raw-id" ? id : "";
|
|
5722
|
+
};
|
|
5723
|
+
const configName = reference("configId", "configName", lookupMaps?.configIdToName);
|
|
5724
|
+
const evaluatorPromptKey = reference("evaluatorPromptId", "evaluatorPromptKey", lookupMaps?.promptIdToKey);
|
|
5725
|
+
// The judge's config belongs to the evaluator prompt, not to this sidecar's
|
|
5726
|
+
// block; config ids are app-unique, so the app-wide map names it.
|
|
5727
|
+
const evaluatorConfigName = reference("evaluatorConfigId", "evaluatorConfigName", lookupMaps?.configIdToName);
|
|
5733
5728
|
// #2644 criterion 1 — the `[test]` key set comes from the vendored
|
|
5734
5729
|
// definition, so a field added there is emitted by pull with no edit here.
|
|
5735
5730
|
// The three reference fields carry the resolved NAME (a test file is portable
|
|
5736
5731
|
// across apps); the rest keep the empty-string / "{}" / "[]" spellings a test
|
|
5737
5732
|
// file has always used for "not set".
|
|
5738
|
-
const
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
}
|
|
5763
|
-
|
|
5733
|
+
const test = projectRecordToToml(TEST_CASE_TABLE, testCase, {
|
|
5734
|
+
name: (record) => record.name || "",
|
|
5735
|
+
description: (record) => record.description || "",
|
|
5736
|
+
inputVariables: (record) => jsonFieldText(record.inputVariables, "{}"),
|
|
5737
|
+
configId: () => configName,
|
|
5738
|
+
evaluatorPromptId: () => evaluatorPromptKey,
|
|
5739
|
+
evaluatorConfigId: () => evaluatorConfigName,
|
|
5740
|
+
expectedOutputPattern: (record) => record.expectedOutputPattern || "",
|
|
5741
|
+
expectedOutputContains: (record) => jsonFieldText(record.expectedOutputContains, "[]"),
|
|
5742
|
+
expectedJsonSubset: (record) => jsonFieldText(record.expectedJsonSubset, "{}"),
|
|
5743
|
+
});
|
|
5744
|
+
return { test, unresolved };
|
|
5745
|
+
}
|
|
5746
|
+
/**
|
|
5747
|
+
* The sidecar `config pull` writes for one server record. It never emits an id
|
|
5748
|
+
* key (#3998): a reference the lookups cannot name is written as an empty name
|
|
5749
|
+
* and reported through `onUnresolved`, and `pullTestCasesForBlock` declines to
|
|
5750
|
+
* write such a case rather than unpin it on disk.
|
|
5751
|
+
*/
|
|
5752
|
+
export function serializeTestCase(testCase, lookupMaps, options = {}) {
|
|
5753
|
+
// #2644 — name any server key this CLI version does not know rather than
|
|
5754
|
+
// dropping it silently on the way into the file. Test cases are a registered
|
|
5755
|
+
// surface; their sidecar shape is the only thing that sets them apart.
|
|
5756
|
+
warnUnrecognizedServerKeys(testCase, TEST_CASE_TABLE, options.file ?? `<block>.tests/${testCase?.name ?? "unnamed"}.toml`, options.logger);
|
|
5757
|
+
const { test, unresolved } = testCaseSidecarTable(testCase, lookupMaps, "empty");
|
|
5758
|
+
if (unresolved.length > 0)
|
|
5759
|
+
options.onUnresolved?.(unresolved);
|
|
5760
|
+
return stringifyConfigToml({ test });
|
|
5761
|
+
}
|
|
5762
|
+
/**
|
|
5763
|
+
* The rendering of a server record that the comparison parses (#3998) — never
|
|
5764
|
+
* written to disk.
|
|
5765
|
+
*
|
|
5766
|
+
* It differs from the file pull writes in one place: a reference the lookups
|
|
5767
|
+
* cannot name carries its raw id in the NAME slot. A live case pinned to a
|
|
5768
|
+
* deleted or unlistable configuration must stay a difference against an
|
|
5769
|
+
* unpinned file; projecting it as "no reference" would read the pair as Synced,
|
|
5770
|
+
* and the next content edit would push `configId: null` and clear the pin in
|
|
5771
|
+
* silence.
|
|
5772
|
+
*/
|
|
5773
|
+
export function serializeTestCaseForComparison(testCase, lookupMaps) {
|
|
5774
|
+
const { test } = testCaseSidecarTable(testCase, lookupMaps, "raw-id");
|
|
5775
|
+
return stringifyConfigToml({ test });
|
|
5764
5776
|
}
|
|
5765
5777
|
/**
|
|
5766
5778
|
* The TOML spelling of a JSON-valued test-case field (#2769).
|
|
@@ -5810,20 +5822,22 @@ export function parseTestCaseToml(tomlData) {
|
|
|
5810
5822
|
description: (value) => (value ? value : undefined),
|
|
5811
5823
|
// Always sent — the server requires it, and `{}` is the empty spelling.
|
|
5812
5824
|
inputVariables: (value) => parseJsonText(value, ["{}"]) ?? {},
|
|
5813
|
-
//
|
|
5814
|
-
//
|
|
5815
|
-
//
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5825
|
+
// A reference is authored by name only: the names become the `_`
|
|
5826
|
+
// markers below, which the push path resolves against the live app. The
|
|
5827
|
+
// id spellings are retired (#3998) and refused by the preflight, so
|
|
5828
|
+
// nothing is read into the wire field here — and these producers must
|
|
5829
|
+
// stay, because without one `buildPayloadFromToml` would forward the
|
|
5830
|
+
// NAME (`configName`) under the id field.
|
|
5831
|
+
configId: () => undefined,
|
|
5832
|
+
evaluatorPromptId: () => undefined,
|
|
5833
|
+
evaluatorConfigId: () => undefined,
|
|
5819
5834
|
expectedOutputPattern: (value) => (value ? value : undefined),
|
|
5820
5835
|
expectedOutputContains: (value) => parseJsonText(value, ["[]"]),
|
|
5821
5836
|
expectedJsonSubset: (value) => parseJsonText(value, ["{}"]),
|
|
5822
5837
|
},
|
|
5823
5838
|
});
|
|
5824
5839
|
// An omitted field must be absent from the body, not present-and-undefined:
|
|
5825
|
-
// the
|
|
5826
|
-
// check reads the body's keys.
|
|
5840
|
+
// the server's unknown-key check reads the body's keys.
|
|
5827
5841
|
for (const key of Object.keys(payload)) {
|
|
5828
5842
|
if (payload[key] === undefined)
|
|
5829
5843
|
delete payload[key];
|
|
@@ -6176,6 +6190,35 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
6176
6190
|
items,
|
|
6177
6191
|
};
|
|
6178
6192
|
}
|
|
6193
|
+
/**
|
|
6194
|
+
* The id→name lookups `config pull` writes test-case references through
|
|
6195
|
+
* (#3998).
|
|
6196
|
+
*
|
|
6197
|
+
* The prompt half is built from EVERY prompt's detail — the pull fetches them
|
|
6198
|
+
* all before `--only` filters the list — so a judge prompt outside the
|
|
6199
|
+
* selection still names. Configurations come from the pulled blocks (a case
|
|
6200
|
+
* pins one of its own block's) and from every prompt (a judge's config is the
|
|
6201
|
+
* judge prompt's). An entry without both an id and a name is skipped.
|
|
6202
|
+
*/
|
|
6203
|
+
export function testCaseLookupMapsForPull(input) {
|
|
6204
|
+
const configIdToName = new Map();
|
|
6205
|
+
const promptIdToKey = new Map();
|
|
6206
|
+
const addConfig = (config) => {
|
|
6207
|
+
if (config?.configId && config?.configName) {
|
|
6208
|
+
configIdToName.set(config.configId, config.configName);
|
|
6209
|
+
}
|
|
6210
|
+
};
|
|
6211
|
+
for (const prompt of input.prompts) {
|
|
6212
|
+
if (prompt?.promptId && prompt?.promptKey) {
|
|
6213
|
+
promptIdToKey.set(prompt.promptId, prompt.promptKey);
|
|
6214
|
+
}
|
|
6215
|
+
for (const config of prompt?.configs ?? [])
|
|
6216
|
+
addConfig(config);
|
|
6217
|
+
}
|
|
6218
|
+
for (const config of input.configs)
|
|
6219
|
+
addConfig(config);
|
|
6220
|
+
return { configIdToName, promptIdToKey };
|
|
6221
|
+
}
|
|
6179
6222
|
/**
|
|
6180
6223
|
* Every test case a block has, draining the cursor (#2769).
|
|
6181
6224
|
*
|
|
@@ -6228,6 +6271,27 @@ export function carryForwardTestCaseEntities(params) {
|
|
|
6228
6271
|
}
|
|
6229
6272
|
return carried;
|
|
6230
6273
|
}
|
|
6274
|
+
/**
|
|
6275
|
+
* Whether two sidecar names in `testsDir` are one file on disk (#3998).
|
|
6276
|
+
*
|
|
6277
|
+
* Names that differ only by case are one file on the case-insensitive
|
|
6278
|
+
* filesystems most of these trees live on and two on the rest (#2896), so the
|
|
6279
|
+
* answer comes from the filesystem itself: the same device and inode.
|
|
6280
|
+
*/
|
|
6281
|
+
function sameTestCaseFile(testsDir, a, b) {
|
|
6282
|
+
if (a === b)
|
|
6283
|
+
return true;
|
|
6284
|
+
if (normalizeTestCaseKey(a) !== normalizeTestCaseKey(b))
|
|
6285
|
+
return false;
|
|
6286
|
+
try {
|
|
6287
|
+
const left = statSync(join(testsDir, `${a}.toml`));
|
|
6288
|
+
const right = statSync(join(testsDir, `${b}.toml`));
|
|
6289
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
6290
|
+
}
|
|
6291
|
+
catch {
|
|
6292
|
+
return false;
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6231
6295
|
/**
|
|
6232
6296
|
* Write one block's test cases into its `<key>.tests/` sidecar and record them
|
|
6233
6297
|
* in sync state. Exported so the unit tests can drive it against a stubbed
|
|
@@ -6243,10 +6307,9 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6243
6307
|
const testsDir = getTestsDir(configDir, blockType, blockKey);
|
|
6244
6308
|
// A block with no cases and no sidecar is not a block whose sidecar is empty:
|
|
6245
6309
|
// creating the directory here would commit an empty folder Git cannot carry.
|
|
6246
|
-
if (testCases.length === 0 && !existsSync(testsDir))
|
|
6247
|
-
return { ok: true, count: 0 };
|
|
6248
|
-
|
|
6249
|
-
ensureDir(testsDir);
|
|
6310
|
+
if (testCases.length === 0 && !existsSync(testsDir)) {
|
|
6311
|
+
return { ok: true, count: 0, unwritten: [] };
|
|
6312
|
+
}
|
|
6250
6313
|
const pulledSlugs = new Set();
|
|
6251
6314
|
// #2896 — a keyed case is written to the file its key names, so pull stops
|
|
6252
6315
|
// renaming what push authored: the repro's `full-refresh-takes-all-history`
|
|
@@ -6256,15 +6319,35 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6256
6319
|
// The key is server data becoming a path: the containment check an
|
|
6257
6320
|
// attachment's filename gets, so nothing lands outside the sidecar dir.
|
|
6258
6321
|
(name) => attachmentWritePath(testsDir, `${name}.toml`) !== null);
|
|
6322
|
+
// #3998 — render every case before anything is written. A reference the
|
|
6323
|
+
// lookups cannot name has no honest spelling in a sidecar (the id keys are
|
|
6324
|
+
// retired, and an empty name would unpin the case on the next push), so such
|
|
6325
|
+
// a case is NOT written: its file, fixtures and record stay as they were.
|
|
6326
|
+
const writable = [];
|
|
6327
|
+
const skipped = [];
|
|
6259
6328
|
for (const tc of testCases) {
|
|
6260
6329
|
const slug = fileNames.get(tc);
|
|
6330
|
+
let references = [];
|
|
6331
|
+
const toml = serializeTestCase(tc, lookupMaps, {
|
|
6332
|
+
file: join(testsDir, `${slug}.toml`),
|
|
6333
|
+
logger: (message) => log(message),
|
|
6334
|
+
onUnresolved: (refs) => {
|
|
6335
|
+
references = refs;
|
|
6336
|
+
},
|
|
6337
|
+
});
|
|
6338
|
+
if (references.length > 0)
|
|
6339
|
+
skipped.push({ tc, slug, references });
|
|
6340
|
+
else
|
|
6341
|
+
writable.push({ tc, slug, toml });
|
|
6342
|
+
}
|
|
6343
|
+
if (writable.length > 0)
|
|
6344
|
+
ensureDir(testsDir);
|
|
6345
|
+
const writtenSlugs = new Map(writable.map((w) => [w.slug, w.tc]));
|
|
6346
|
+
for (const { tc, slug, toml } of writable) {
|
|
6261
6347
|
pulledSlugs.add(slug);
|
|
6262
6348
|
// Write test case TOML
|
|
6263
6349
|
const tomlPath = join(testsDir, `${slug}.toml`);
|
|
6264
|
-
writeFileSync(tomlPath,
|
|
6265
|
-
file: tomlPath,
|
|
6266
|
-
logger: (message) => log(message),
|
|
6267
|
-
}));
|
|
6350
|
+
writeFileSync(tomlPath, toml);
|
|
6268
6351
|
// Attachments: the sidecar directory is the authored set, so pull makes it
|
|
6269
6352
|
// match the server — downloading what is there, recording each file's
|
|
6270
6353
|
// content hash (so push can see a byte change under an unchanged name), and
|
|
@@ -6356,12 +6439,79 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6356
6439
|
}),
|
|
6357
6440
|
};
|
|
6358
6441
|
}
|
|
6442
|
+
// #3998 — a skipped case is found by its ID in the prior state, not by the
|
|
6443
|
+
// file name it would get now: the server can rename a case's key between
|
|
6444
|
+
// pulls, and the old file is the one holding the pin. Every such file is
|
|
6445
|
+
// kept out of the cleanup below and every such record carried forward under
|
|
6446
|
+
// its existing key — unless a case written above now owns that file, whose
|
|
6447
|
+
// write wins it (the key-to-file rule of #2896). "That file" is decided by
|
|
6448
|
+
// the filesystem: `Alpha` and `alpha` are one file where names fold case.
|
|
6449
|
+
const unwritten = [];
|
|
6450
|
+
const writtenOwner = (candidate) => writtenSlugs.get(candidate) ??
|
|
6451
|
+
writable.find((w) => sameTestCaseFile(testsDir, w.slug, candidate))?.tc;
|
|
6452
|
+
for (const { tc, slug, references } of skipped) {
|
|
6453
|
+
const priorEntries = Object.entries(params.priorTestCaseEntities ?? {}).filter(([, entry]) => entry?.blockType === blockType &&
|
|
6454
|
+
entry?.blockKey === blockKey &&
|
|
6455
|
+
entry?.id === tc.testCaseId);
|
|
6456
|
+
const slugs = new Set([slug]);
|
|
6457
|
+
for (const [stateKey, entry] of priorEntries) {
|
|
6458
|
+
slugs.add(entry.slug ?? stateKey.slice(stateKey.lastIndexOf("/") + 1));
|
|
6459
|
+
}
|
|
6460
|
+
const preservedSlugs = [];
|
|
6461
|
+
const superseded = new Set();
|
|
6462
|
+
for (const candidate of slugs) {
|
|
6463
|
+
const owner = writtenOwner(candidate);
|
|
6464
|
+
if (owner) {
|
|
6465
|
+
superseded.add(candidate);
|
|
6466
|
+
warn(` ${blockType}: ${blockKey}/${candidate}.toml now holds test case ` +
|
|
6467
|
+
`"${owner?.name ?? ""}" — the prior sidecar of unwritten test case ` +
|
|
6468
|
+
`"${tc?.name ?? ""}" at that path was superseded`);
|
|
6469
|
+
continue;
|
|
6470
|
+
}
|
|
6471
|
+
pulledSlugs.add(candidate);
|
|
6472
|
+
if (existsSync(join(testsDir, `${candidate}.toml`))) {
|
|
6473
|
+
preservedSlugs.push(candidate);
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
for (const [stateKey, entry] of priorEntries) {
|
|
6477
|
+
const entrySlug = entry.slug ?? stateKey.slice(stateKey.lastIndexOf("/") + 1);
|
|
6478
|
+
if (superseded.has(entrySlug))
|
|
6479
|
+
continue;
|
|
6480
|
+
if (testCaseEntities[stateKey])
|
|
6481
|
+
continue;
|
|
6482
|
+
testCaseEntities[stateKey] = entry;
|
|
6483
|
+
}
|
|
6484
|
+
const what = references
|
|
6485
|
+
.map((ref) => `${ref.tomlKey} (id ${ref.id})`)
|
|
6486
|
+
.join(", ");
|
|
6487
|
+
const kept = preservedSlugs.length > 0
|
|
6488
|
+
? `kept ${preservedSlugs.map((s) => `${s}.toml`).join(", ")} as it was`
|
|
6489
|
+
: "no sidecar exists for it";
|
|
6490
|
+
warn(` Test case not written for ${blockType}: ${blockKey}/${slug} ` +
|
|
6491
|
+
`("${tc?.name ?? ""}"): ${what} could not be resolved to a name — ` +
|
|
6492
|
+
`${kept}, so the pin is not lost. The configuration or prompt may have ` +
|
|
6493
|
+
"been deleted (re-pin the case on the server), or its listing failed " +
|
|
6494
|
+
"on this pull (run `config pull` again).");
|
|
6495
|
+
unwritten.push({
|
|
6496
|
+
blockType,
|
|
6497
|
+
blockKey,
|
|
6498
|
+
slug,
|
|
6499
|
+
name: tc?.name ?? "",
|
|
6500
|
+
testCaseId: tc?.testCaseId,
|
|
6501
|
+
references,
|
|
6502
|
+
preservedSlugs,
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
6359
6505
|
// Clean up local files for remotely-deleted test cases
|
|
6360
6506
|
if (existsSync(testsDir)) {
|
|
6361
6507
|
const localFiles = readdirSync(testsDir).filter((f) => f.endsWith(".toml"));
|
|
6362
6508
|
for (const file of localFiles) {
|
|
6363
6509
|
const localSlug = basename(file, ".toml");
|
|
6364
|
-
|
|
6510
|
+
// A listing keeps the casing a file was created with, so the name a case
|
|
6511
|
+
// was just written to can come back as `Alpha.toml` — the same file.
|
|
6512
|
+
const pulled = pulledSlugs.has(localSlug) ||
|
|
6513
|
+
[...pulledSlugs].some((slug) => sameTestCaseFile(testsDir, slug, localSlug));
|
|
6514
|
+
if (!pulled) {
|
|
6365
6515
|
// Remove orphaned TOML
|
|
6366
6516
|
unlinkSync(join(testsDir, file));
|
|
6367
6517
|
// Remove orphaned attachment dir if exists
|
|
@@ -6381,7 +6531,7 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6381
6531
|
}
|
|
6382
6532
|
}
|
|
6383
6533
|
}
|
|
6384
|
-
return { ok: true, count:
|
|
6534
|
+
return { ok: true, count: writable.length, unwritten };
|
|
6385
6535
|
}
|
|
6386
6536
|
/**
|
|
6387
6537
|
* One pull leg: every block of a type the pull selected (#2769).
|
|
@@ -6395,6 +6545,7 @@ export async function pullTestCasesForBlocks(params) {
|
|
|
6395
6545
|
const log = params.logger ?? ((message) => info(message));
|
|
6396
6546
|
let count = 0;
|
|
6397
6547
|
const skippedBlocks = [];
|
|
6548
|
+
const unwritten = [];
|
|
6398
6549
|
for (const block of params.blocks) {
|
|
6399
6550
|
const outcome = await pullTestCasesForBlock({
|
|
6400
6551
|
client: params.client,
|
|
@@ -6422,9 +6573,14 @@ export async function pullTestCasesForBlocks(params) {
|
|
|
6422
6573
|
if (outcome.count > 0) {
|
|
6423
6574
|
log(` Wrote ${outcome.count} test case(s) for ${params.blockType}: ${block.key}`);
|
|
6424
6575
|
}
|
|
6576
|
+
if (outcome.unwritten.length > 0) {
|
|
6577
|
+
log(` ${outcome.unwritten.length} test case(s) not written for ` +
|
|
6578
|
+
`${params.blockType}: ${block.key} (unresolved references; see above)`);
|
|
6579
|
+
}
|
|
6425
6580
|
count += outcome.count;
|
|
6581
|
+
unwritten.push(...outcome.unwritten);
|
|
6426
6582
|
}
|
|
6427
|
-
return { count, skippedBlocks };
|
|
6583
|
+
return { count, skippedBlocks, unwritten };
|
|
6428
6584
|
}
|
|
6429
6585
|
/** The 10 MB per-attachment cap the server enforces. */
|
|
6430
6586
|
const TEN_MB = 10 * 1024 * 1024;
|
|
@@ -6616,9 +6772,20 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6616
6772
|
continue;
|
|
6617
6773
|
}
|
|
6618
6774
|
const tomlData = parseTomlFile(filePath);
|
|
6775
|
+
// #3998 — the command's preflight refuses a sidecar it cannot push, but
|
|
6776
|
+
// this leg is exported and the parser no longer reads the retired id keys:
|
|
6777
|
+
// a file reaching it unchecked would be pushed UNPINNED in silence. The
|
|
6778
|
+
// same per-loop defense the workflow leg keeps (#976).
|
|
6779
|
+
const sidecarErrors = testCaseSidecarPreflightErrors(filePath, tomlData);
|
|
6780
|
+
if (sidecarErrors.length > 0) {
|
|
6781
|
+
const message = sidecarErrors.join("; ");
|
|
6782
|
+
warn(` Failed to push test case ${blockKey}/${slug}: ${message}`);
|
|
6783
|
+
failures.push({ type: "test-case", key: `${blockKey}/${slug}`, message });
|
|
6784
|
+
continue;
|
|
6785
|
+
}
|
|
6619
6786
|
const testPayload = parseTestCaseToml(tomlData);
|
|
6620
|
-
// Resolve
|
|
6621
|
-
// resolve is a FAILURE: silently dropping it would push the test case with
|
|
6787
|
+
// Resolve the authored names to IDs — a name is the only spelling a
|
|
6788
|
+
// sidecar has (#3998). An authored name that does not resolve is a FAILURE: silently dropping it would push the test case with
|
|
6622
6789
|
// no pin, quietly changing what the case tests (#2769). The exception is a
|
|
6623
6790
|
// preview of a block this push has not created yet — its configs cannot
|
|
6624
6791
|
// exist to be named, so a dry run reports the reference as pending rather
|
|
@@ -6639,7 +6806,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6639
6806
|
delete testPayload._configName;
|
|
6640
6807
|
delete testPayload._evaluatorPromptKey;
|
|
6641
6808
|
delete testPayload._evaluatorConfigName;
|
|
6642
|
-
if (configName
|
|
6809
|
+
if (configName) {
|
|
6643
6810
|
const configMap = blockType === "prompt"
|
|
6644
6811
|
? resolutionMaps?.promptConfigNameToId
|
|
6645
6812
|
: blockType === "script"
|
|
@@ -6657,14 +6824,14 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6657
6824
|
else
|
|
6658
6825
|
noteUnresolved(`configName "${configName}"`, blockType, blockKey);
|
|
6659
6826
|
}
|
|
6660
|
-
if (evaluatorPromptKey
|
|
6827
|
+
if (evaluatorPromptKey) {
|
|
6661
6828
|
const resolvedId = resolutionMaps?.promptKeyToId.get(evaluatorPromptKey);
|
|
6662
6829
|
if (resolvedId)
|
|
6663
6830
|
testPayload.evaluatorPromptId = resolvedId;
|
|
6664
6831
|
else
|
|
6665
6832
|
noteUnresolved(`evaluatorPromptKey "${evaluatorPromptKey}"`, "prompt", evaluatorPromptKey);
|
|
6666
6833
|
}
|
|
6667
|
-
if (evaluatorConfigName
|
|
6834
|
+
if (evaluatorConfigName) {
|
|
6668
6835
|
let resolvedId = evaluatorPromptKey
|
|
6669
6836
|
? resolutionMaps?.promptConfigNameToId.get(`${evaluatorPromptKey}#${evaluatorConfigName}`)
|
|
6670
6837
|
: undefined;
|
|
@@ -7245,9 +7412,10 @@ export function pairTestCases(input) {
|
|
|
7245
7412
|
* integration and workflow sidecar pinned to a config reported Modified
|
|
7246
7413
|
* forever, and push read the same pair as server drift.
|
|
7247
7414
|
*
|
|
7248
|
-
* One listing per block that has sidecars, and a failed one is silent:
|
|
7249
|
-
* unresolved reference compares as
|
|
7250
|
-
*
|
|
7415
|
+
* One listing per block that has sidecars, and a failed one is silent: the
|
|
7416
|
+
* live side's unresolved reference then compares as its raw id
|
|
7417
|
+
* (`serializeTestCaseForComparison`), so the row reads Modified rather than
|
|
7418
|
+
* hiding a pin.
|
|
7251
7419
|
*/
|
|
7252
7420
|
async function fillBlockConfigNames(client, appId, blockType, blockId, configIdToName) {
|
|
7253
7421
|
try {
|
|
@@ -7305,10 +7473,6 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7305
7473
|
});
|
|
7306
7474
|
for (const slug of localSlugs) {
|
|
7307
7475
|
const live = paired.bySlug.get(slug);
|
|
7308
|
-
if (!live) {
|
|
7309
|
-
rows.push({ ...base, slug, status: LOCAL_ONLY_NEW });
|
|
7310
|
-
continue;
|
|
7311
|
-
}
|
|
7312
7476
|
const filePath = join(testsDir, `${slug}.toml`);
|
|
7313
7477
|
let row;
|
|
7314
7478
|
try {
|
|
@@ -7316,7 +7480,9 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7316
7480
|
// #2880 criterion 1 — a sidecar push's preflight would reject is not one
|
|
7317
7481
|
// this command calls synced, and the message is the same one. The WHOLE
|
|
7318
7482
|
// preflight, not just the sidecar's own rules: an unknown key or a
|
|
7319
|
-
// mistyped value aborts push too.
|
|
7483
|
+
// mistyped value aborts push too. #3998 — and an UNPAIRED sidecar is
|
|
7484
|
+
// checked before it is called local-only, so a new file push would refuse
|
|
7485
|
+
// (a retired id key) is reported with the guidance, not as a create.
|
|
7320
7486
|
const errors = testCaseSidecarPreflightErrors(filePath, tomlData);
|
|
7321
7487
|
if (errors.length > 0) {
|
|
7322
7488
|
row = {
|
|
@@ -7326,6 +7492,10 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7326
7492
|
hint: errors.join("; "),
|
|
7327
7493
|
};
|
|
7328
7494
|
}
|
|
7495
|
+
else if (!live) {
|
|
7496
|
+
rows.push({ ...base, slug, status: LOCAL_ONLY_NEW });
|
|
7497
|
+
continue;
|
|
7498
|
+
}
|
|
7329
7499
|
else {
|
|
7330
7500
|
const verdict = compareLocalToRemote(spec, tomlData, live, { ruleSetIdToName: new Map(), ruleSetNameToId: new Map() }, extra,
|
|
7331
7501
|
// Both sides read a reference through the same lookups — see
|
|
@@ -7342,6 +7512,12 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7342
7512
|
hint: `${filePath}: ${err?.message || String(err)}`,
|
|
7343
7513
|
};
|
|
7344
7514
|
}
|
|
7515
|
+
// An unpaired file push would refuse has no attachment plan, as a
|
|
7516
|
+
// local-only one has none.
|
|
7517
|
+
if (!live) {
|
|
7518
|
+
rows.push(row);
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7345
7521
|
// #2880 behavior 19 — attachments are part of what push would do, read
|
|
7346
7522
|
// from the SAME plan push executes. A sidecar whose text compares clean but
|
|
7347
7523
|
// whose fixtures have not been uploaded is not Synced.
|
|
@@ -7817,7 +7993,11 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7817
7993
|
const appWorkflowItems = selectPull("workflow", workflowItems.filter((w) => !isPlatformOwnedWorkflow(w)), (w) => w.workflowKey);
|
|
7818
7994
|
// Fetch details for each entity
|
|
7819
7995
|
const integrations = selectPull("integration", await Promise.all(integrationItems.map((i) => client.getIntegration(resolvedAppId, i.integrationId))), (i) => i.integrationKey);
|
|
7820
|
-
|
|
7996
|
+
// #3998 — every prompt's detail is kept, not just the selection's: a
|
|
7997
|
+
// test case of a selected block may be judged by a prompt `--only`
|
|
7998
|
+
// left out, and its sidecar names that prompt by key.
|
|
7999
|
+
const promptDetails = await Promise.all(promptItems.map((p) => client.getPrompt(resolvedAppId, p.promptId)));
|
|
8000
|
+
const prompts = selectPull("prompt", promptDetails, (p) => p.promptKey);
|
|
7821
8001
|
const workflows = await Promise.all(appWorkflowItems.map(async (w) => {
|
|
7822
8002
|
const workflowData = await client.getWorkflow(resolvedAppId, w.workflowId);
|
|
7823
8003
|
// #2645 — fetch EVERY config's body, not just the active one. The
|
|
@@ -8318,57 +8498,38 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8318
8498
|
if (pulledFunctionsCount > 0) {
|
|
8319
8499
|
info(` Pulled ${pulledFunctionsCount} function(s)`);
|
|
8320
8500
|
}
|
|
8321
|
-
//
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
// Also include workflow config names
|
|
8333
|
-
for (const { configs } of workflows) {
|
|
8334
|
-
if (configs) {
|
|
8335
|
-
for (const config of configs) {
|
|
8336
|
-
configIdToName.set(config.configId, config.configName);
|
|
8337
|
-
}
|
|
8338
|
-
}
|
|
8339
|
-
}
|
|
8340
|
-
// Include script config names so a pinned script test case
|
|
8341
|
-
// round-trips its `configName` reference (the same way prompt/workflow
|
|
8342
|
-
// configs above do). One extra `configs` fetch per pulled script.
|
|
8343
|
-
for (const [name, entity] of Object.entries(scriptEntities)) {
|
|
8501
|
+
// Lookup maps for test case serialization (#3998): every prompt's
|
|
8502
|
+
// detail, so a judge prompt outside an `--only` selection still names,
|
|
8503
|
+
// and the configurations of the pulled blocks — a case pins one of its
|
|
8504
|
+
// own block's. A reference these cannot resolve is reported by
|
|
8505
|
+
// `pullTestCasesForBlock`, and its sidecar is not written.
|
|
8506
|
+
const blockConfigs = [];
|
|
8507
|
+
for (const { configs } of workflows)
|
|
8508
|
+
blockConfigs.push(...(configs ?? []));
|
|
8509
|
+
// One `configs` fetch per pulled script and integration. A failed one
|
|
8510
|
+
// is non-fatal: the references it would have named are reported.
|
|
8511
|
+
for (const entity of Object.values(scriptEntities)) {
|
|
8344
8512
|
try {
|
|
8345
|
-
const { items
|
|
8346
|
-
|
|
8347
|
-
configIdToName.set(config.configId, config.configName);
|
|
8348
|
-
}
|
|
8513
|
+
const { items } = await client.listScriptConfigs(resolvedAppId, entity.id);
|
|
8514
|
+
blockConfigs.push(...items);
|
|
8349
8515
|
}
|
|
8350
8516
|
catch {
|
|
8351
|
-
// Non-fatal
|
|
8352
|
-
// its legacy id spelling instead of a name (#2769), so the pin
|
|
8353
|
-
// survives. The header/body still pulled fine — `name` retained.
|
|
8354
|
-
void name;
|
|
8517
|
+
// Non-fatal — see above.
|
|
8355
8518
|
}
|
|
8356
8519
|
}
|
|
8357
|
-
// Integration config names, for the same reason (#2769). A failed fetch
|
|
8358
|
-
// is non-fatal here too: `serializeTestCase` keeps the pin as `configId`
|
|
8359
|
-
// rather than writing an empty reference.
|
|
8360
8520
|
for (const integration of integrations) {
|
|
8361
8521
|
try {
|
|
8362
|
-
const { items
|
|
8363
|
-
|
|
8364
|
-
configIdToName.set(config.configId, config.configName);
|
|
8365
|
-
}
|
|
8522
|
+
const { items } = await client.listIntegrationConfigs(resolvedAppId, integration.integrationId);
|
|
8523
|
+
blockConfigs.push(...items);
|
|
8366
8524
|
}
|
|
8367
8525
|
catch {
|
|
8368
8526
|
// Non-fatal — see above.
|
|
8369
8527
|
}
|
|
8370
8528
|
}
|
|
8371
|
-
const testCaseLookupMaps = {
|
|
8529
|
+
const testCaseLookupMaps = testCaseLookupMapsForPull({
|
|
8530
|
+
prompts: promptDetails,
|
|
8531
|
+
configs: blockConfigs,
|
|
8532
|
+
});
|
|
8372
8533
|
// #2731 B4 — pull is one of the two moments local and server provably
|
|
8373
8534
|
// agree, so it stamps the semantic baseline beside the byte hash (push
|
|
8374
8535
|
// stamps the other one, from the entity the server returned). The
|
|
@@ -8408,6 +8569,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8408
8569
|
const testCaseEntities = {};
|
|
8409
8570
|
const priorTestCaseEntities = priorEntities.testCases;
|
|
8410
8571
|
let totalTestCases = 0;
|
|
8572
|
+
let unwrittenTestCases = 0;
|
|
8411
8573
|
for (const [blockType, blocks] of [
|
|
8412
8574
|
[
|
|
8413
8575
|
"prompt",
|
|
@@ -8437,7 +8599,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8437
8599
|
})),
|
|
8438
8600
|
],
|
|
8439
8601
|
]) {
|
|
8440
|
-
const { count } = await pullTestCasesForBlocks({
|
|
8602
|
+
const { count, unwritten } = await pullTestCasesForBlocks({
|
|
8441
8603
|
client,
|
|
8442
8604
|
appId: resolvedAppId,
|
|
8443
8605
|
configDir,
|
|
@@ -8449,6 +8611,14 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8449
8611
|
logger: (message) => info(message),
|
|
8450
8612
|
});
|
|
8451
8613
|
totalTestCases += count;
|
|
8614
|
+
unwrittenTestCases += unwritten.length;
|
|
8615
|
+
}
|
|
8616
|
+
// #3998 — a case pull could not name a reference for is not written, so
|
|
8617
|
+
// its pin is never lost; the count is repeated here so it is not missed
|
|
8618
|
+
// among the per-block lines. Exit status is unchanged, as for every
|
|
8619
|
+
// other partial fetch this command reports.
|
|
8620
|
+
if (unwrittenTestCases > 0) {
|
|
8621
|
+
warn(`${unwrittenTestCases} test case(s) not written — references could not be resolved; see above`);
|
|
8452
8622
|
}
|
|
8453
8623
|
// Build ruleSetId → name map for database types and group type configs
|
|
8454
8624
|
const ruleSets = selectPull("rule-set", Array.isArray(ruleSetsResult) ? ruleSetsResult : [], (rs) => ruleSetFileKey(rs));
|
|
@@ -8966,6 +9136,9 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8966
9136
|
keyValue("Functions", pulledFunctionsCount);
|
|
8967
9137
|
keyValue("Email Templates", emailTemplates.length);
|
|
8968
9138
|
keyValue("Test Cases", totalTestCases);
|
|
9139
|
+
if (unwrittenTestCases > 0) {
|
|
9140
|
+
keyValue("Test Cases not written", unwrittenTestCases);
|
|
9141
|
+
}
|
|
8969
9142
|
keyValue("Database Types", databaseTypesWithOps.length);
|
|
8970
9143
|
keyValue("Rule Sets", ruleSets.length);
|
|
8971
9144
|
keyValue("Group Type Configs", groupTypeConfigs.length);
|