primitive-admin 1.1.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 +14 -13
- package/assets/skill/skills/primitive-platform/SKILL.md +6 -0
- package/dist/src/commands/collections.js +20 -17
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/connections.js +1 -5
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/database-types.js +13 -313
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +7 -128
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/groups.js +16 -0
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/llm.js +9 -6
- package/dist/src/commands/llm.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/settings.d.ts +18 -0
- package/dist/src/commands/settings.js +54 -0
- package/dist/src/commands/settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +77 -7
- package/dist/src/commands/sync.js +368 -198
- 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/commands/workflows.js +3 -3
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +3 -70
- package/dist/src/lib/api-client.js +20 -106
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-match-guard.d.ts +44 -0
- package/dist/src/lib/app-match-guard.js +72 -0
- package/dist/src/lib/app-match-guard.js.map +1 -0
- package/dist/src/lib/collection-export.d.ts +37 -9
- package/dist/src/lib/collection-export.js +48 -12
- package/dist/src/lib/collection-export.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/function-triggers.d.ts +66 -0
- package/dist/src/lib/function-triggers.js +285 -0
- package/dist/src/lib/function-triggers.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +0 -1
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +4 -2
- package/dist/src/lib/generated-config-surfaces.js +138 -68
- 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/dist/src/lib/generated-workflow-model-fields.d.ts +20 -0
- package/dist/src/lib/generated-workflow-model-fields.js +53 -0
- package/dist/src/lib/generated-workflow-model-fields.js.map +1 -0
- package/dist/src/lib/google-client-secret-status.d.ts +35 -0
- package/dist/src/lib/google-client-secret-status.js +56 -0
- package/dist/src/lib/google-client-secret-status.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +1 -6
- package/dist/src/lib/log-inspection.js +5 -7
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +4 -3
- package/dist/src/lib/migration-nag.js.map +1 -1
- package/dist/src/lib/paginate.d.ts +8 -24
- package/dist/src/lib/paginate.js +8 -21
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/sync-dir-selector.d.ts +1 -1
- package/dist/src/lib/sync-dir-selector.js +1 -1
- package/dist/src/lib/toml-database-config.js +2 -2
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/workflow-field-descriptor.d.ts +91 -0
- package/dist/src/lib/workflow-field-descriptor.js +153 -0
- package/dist/src/lib/workflow-field-descriptor.js.map +1 -0
- 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];
|
|
@@ -1698,12 +1692,9 @@ const PROMPT_CONFIG_DEFAULT_STATUS = fieldServerDefault(PROMPT_CONFIG_TABLE, "st
|
|
|
1698
1692
|
* first-wins rule: making the file's meaning depend on entry order is the kind
|
|
1699
1693
|
* of implicit behavior this issue exists to remove, and silently picking one
|
|
1700
1694
|
* would let the repo claim something the server does not do.
|
|
1701
|
-
*
|
|
1702
|
-
* `isActive` is accepted as a legacy spelling — the create leg read it before
|
|
1703
|
-
* pull ever wrote a marker, so a hand-authored file carrying it keeps working.
|
|
1704
1695
|
*/
|
|
1705
1696
|
export function promptActiveConfigName(configs) {
|
|
1706
|
-
const marked = (configs || []).filter((c) => c?.active === true
|
|
1697
|
+
const marked = (configs || []).filter((c) => c?.active === true);
|
|
1707
1698
|
if (marked.length > 1) {
|
|
1708
1699
|
throw new Error(`Only one [[configs]] entry may set active = true; found ${marked.length} ` +
|
|
1709
1700
|
`(${marked.map((c) => c.name).join(", ")}).`);
|
|
@@ -1764,9 +1755,6 @@ export function buildBlobBucketCreatePayload(key, bucket) {
|
|
|
1764
1755
|
},
|
|
1765
1756
|
}),
|
|
1766
1757
|
};
|
|
1767
|
-
if (!payload.preset && bucket?.accessPolicy) {
|
|
1768
|
-
payload.accessPolicy = bucket.accessPolicy;
|
|
1769
|
-
}
|
|
1770
1758
|
for (const field of Object.keys(payload)) {
|
|
1771
1759
|
if (payload[field] === undefined)
|
|
1772
1760
|
delete payload[field];
|
|
@@ -4012,7 +4000,7 @@ export function parseDatabaseTypeToml(tomlData) {
|
|
|
4012
4000
|
},
|
|
4013
4001
|
}));
|
|
4014
4002
|
canonicalizeDatabaseTypeConfig(typeConfig);
|
|
4015
|
-
//
|
|
4003
|
+
// The `ruleSetName` reference; it resolves to an id later.
|
|
4016
4004
|
applyRuleSetReference(typeSection, typeConfig);
|
|
4017
4005
|
if (tomlData.triggers) {
|
|
4018
4006
|
typeConfig.triggers = tomlData.triggers;
|
|
@@ -4081,13 +4069,16 @@ export function parseRuleSetToml(tomlData) {
|
|
|
4081
4069
|
/**
|
|
4082
4070
|
* Read a config's rule-set reference off its TOML table (#2644).
|
|
4083
4071
|
*
|
|
4084
|
-
*
|
|
4085
|
-
* `ruleSetId` — and the name form is left as `_ruleSetName` for
|
|
4072
|
+
* One spelling, the portable `ruleSetName`, left as `_ruleSetName` for
|
|
4086
4073
|
* `resolveRuleSetReference` to turn into an id once the app's rule sets are
|
|
4087
|
-
* known.
|
|
4088
|
-
*
|
|
4074
|
+
* known. Absent means no reference, which push sends as an explicit `null` so
|
|
4075
|
+
* removing the line clears it (#1567). The id spelling `ruleSetId` is retired
|
|
4076
|
+
* (#3997): the preflight refuses it through `retired-keys.ts`, and it is never
|
|
4077
|
+
* read here, so no id authored in TOML reaches the wire.
|
|
4089
4078
|
*/
|
|
4090
4079
|
function applyRuleSetReference(section, entity) {
|
|
4080
|
+
// The payload builder put the `ruleSetName` value on the `ruleSetId` field;
|
|
4081
|
+
// the resolver sets the real id.
|
|
4091
4082
|
delete entity.ruleSetId;
|
|
4092
4083
|
if (section.ruleSetName && section.ruleSetName !== "") {
|
|
4093
4084
|
// Trimmed to match the name the server stores for the rule set itself
|
|
@@ -4097,9 +4088,6 @@ function applyRuleSetReference(section, entity) {
|
|
|
4097
4088
|
? section.ruleSetName.trim()
|
|
4098
4089
|
: section.ruleSetName;
|
|
4099
4090
|
}
|
|
4100
|
-
else if (section.ruleSetId && section.ruleSetId !== "") {
|
|
4101
|
-
entity.ruleSetId = section.ruleSetId;
|
|
4102
|
-
}
|
|
4103
4091
|
}
|
|
4104
4092
|
/** Drop keys the TOML did not carry, so `"x" in entity` stays meaningful. */
|
|
4105
4093
|
function dropUndefined(entity) {
|
|
@@ -4162,22 +4150,22 @@ export function parseCollectionTypeConfigToml(tomlData) {
|
|
|
4162
4150
|
// the SAME push parser before hashing.
|
|
4163
4151
|
//
|
|
4164
4152
|
// Parsing through the push parser — rather than hashing the raw parsed TOML —
|
|
4165
|
-
// is what makes the comparison encoding-independent, and is why the
|
|
4153
|
+
// is what makes the comparison encoding-independent, and is why the known
|
|
4166
4154
|
// false-diff traps do not fire: a legacy JSON-string operation and the native
|
|
4167
4155
|
// `[operations.definition]` table the server emits normalize to one shape
|
|
4168
|
-
// (`normalizeOperationFromToml`); a `ruleSetName` reference
|
|
4169
|
-
//
|
|
4170
|
-
//
|
|
4171
|
-
//
|
|
4172
|
-
//
|
|
4173
|
-
//
|
|
4156
|
+
// (`normalizeOperationFromToml`); a `ruleSetName` reference resolves to the id
|
|
4157
|
+
// the server holds (`normalizeRuleSetRefForDiff`); an omitted `autoAddCreator`
|
|
4158
|
+
// picks up the same default the server serializes (`parseGroupTypeConfigToml`);
|
|
4159
|
+
// and a metadata-category's identity is read from the parsed
|
|
4160
|
+
// `[metadataCategoryConfig]` content, not the filename. Hashing raw parsed TOML
|
|
4161
|
+
// would show a false `Modified` for each of those equivalent forms.
|
|
4174
4162
|
/**
|
|
4175
4163
|
* Resolve a parsed entity's rule-set reference to a stable id for hashing.
|
|
4176
4164
|
* `parseGroupTypeConfigToml` / `parseCollectionTypeConfigToml` /
|
|
4177
|
-
* `parseDatabaseTypeToml` leave
|
|
4178
|
-
*
|
|
4179
|
-
*
|
|
4180
|
-
*
|
|
4165
|
+
* `parseDatabaseTypeToml` leave the `ruleSetName` reference as `_ruleSetName`;
|
|
4166
|
+
* resolving it to `ruleSetId` here means the local file and the server entity
|
|
4167
|
+
* hash the same reference. `throwOnMissing: false` so an unresolvable name
|
|
4168
|
+
* degrades gracefully rather than aborting the diff.
|
|
4181
4169
|
*
|
|
4182
4170
|
* When the name does NOT resolve — a typo, or a rule set added locally in the
|
|
4183
4171
|
* same tree but not yet on the server — `resolveRuleSetReference` deletes
|
|
@@ -4538,16 +4526,16 @@ function jsonFieldsForDiff(table, entity) {
|
|
|
4538
4526
|
/**
|
|
4539
4527
|
* One test-case reference as the comparison reads it (#2880 behavior 17).
|
|
4540
4528
|
*
|
|
4541
|
-
* A reference reaches the projection in
|
|
4542
|
-
* 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:
|
|
4543
4532
|
*
|
|
4544
|
-
* - a portable NAME (`configName = "default"`)
|
|
4545
|
-
*
|
|
4546
|
-
* - a
|
|
4547
|
-
*
|
|
4548
|
-
*
|
|
4549
|
-
*
|
|
4550
|
-
* 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).
|
|
4551
4539
|
*/
|
|
4552
4540
|
function testCaseReference(value, idToName) {
|
|
4553
4541
|
if (typeof value !== "string" || value === "")
|
|
@@ -4824,14 +4812,6 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4824
4812
|
table: BLOB_BUCKET_TABLE,
|
|
4825
4813
|
parse: (doc) => {
|
|
4826
4814
|
const bucket = { ...(doc?.bucket ?? {}) };
|
|
4827
|
-
// #1020 — `accessPolicy` is the pre-`preset` spelling, still accepted on
|
|
4828
|
-
// the wire and still authored in unmigrated files. Both spellings mean
|
|
4829
|
-
// the same access model, so they project the same way; without this an
|
|
4830
|
-
// unmigrated file reports a difference no push can clear (push sends the
|
|
4831
|
-
// legacy key and the server answers with `preset`).
|
|
4832
|
-
if (bucket.preset === undefined && bucket.accessPolicy !== undefined) {
|
|
4833
|
-
bucket.preset = bucket.accessPolicy;
|
|
4834
|
-
}
|
|
4835
4815
|
return {
|
|
4836
4816
|
bucketKey: doc?.bucket?.key ?? "",
|
|
4837
4817
|
...buildPayloadFromToml(BLOB_BUCKET_TABLE, bucket, {
|
|
@@ -4865,7 +4845,7 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4865
4845
|
const keys = [];
|
|
4866
4846
|
if (!bucket.name)
|
|
4867
4847
|
keys.push("name");
|
|
4868
|
-
if (!bucket.preset
|
|
4848
|
+
if (!bucket.preset)
|
|
4869
4849
|
keys.push("preset");
|
|
4870
4850
|
if (!bucket.ruleSetId)
|
|
4871
4851
|
keys.push("ruleSetId");
|
|
@@ -4877,17 +4857,15 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4877
4857
|
// edited expectation read as Synced while push applied it from the file's
|
|
4878
4858
|
// byte hash.
|
|
4879
4859
|
//
|
|
4880
|
-
// The type's own difficulty is that a reference
|
|
4881
|
-
//
|
|
4882
|
-
//
|
|
4883
|
-
//
|
|
4884
|
-
// `extra` context is what makes that possible (the id→name lookups are not in
|
|
4885
|
-
// 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.
|
|
4886
4864
|
"test-case": {
|
|
4887
4865
|
label: "test-case",
|
|
4888
4866
|
table: TEST_CASE_TABLE,
|
|
4889
4867
|
parse: (doc) => parseTestCaseToml(doc),
|
|
4890
|
-
serialize: (record, _maps, extra) =>
|
|
4868
|
+
serialize: (record, _maps, extra) => serializeTestCaseForComparison(record, extra?.lookupMaps),
|
|
4891
4869
|
// The server trims a test-case name on write (`src/admin-api.ts`), so a
|
|
4892
4870
|
// padded one in the file is not a difference — it is what the server will
|
|
4893
4871
|
// store (#2880 DSO-003).
|
|
@@ -4897,12 +4875,11 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4897
4875
|
},
|
|
4898
4876
|
extras: (entity, _doc, extra) => ({
|
|
4899
4877
|
// These three OVERRIDE the definition's fields of the same name: the
|
|
4900
|
-
// value compared is the thing referred to,
|
|
4901
|
-
//
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
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),
|
|
4906
4883
|
}),
|
|
4907
4884
|
describe: (entity) => `test case "${entity?.name}"`,
|
|
4908
4885
|
},
|
|
@@ -5066,7 +5043,7 @@ export function hashLocalConfigForDiff(spec, parsedToml, maps, extra) {
|
|
|
5066
5043
|
* Hash a SERVER entity the same way, by serializing it into the file `sync
|
|
5067
5044
|
* pull` would write and hashing that — so the two sides are normalized
|
|
5068
5045
|
* identically by construction, and a legacy encoding on disk (a JSON-string
|
|
5069
|
-
* operation
|
|
5046
|
+
* operation) is not a false `Modified`.
|
|
5070
5047
|
*
|
|
5071
5048
|
* `extra` carries the sibling rows a type's file also holds: a database type's
|
|
5072
5049
|
* operations and subscriptions, which the list response does not include.
|
|
@@ -5721,49 +5698,81 @@ export function getTestsDir(configDir, blockType, blockKey) {
|
|
|
5721
5698
|
function blockSelectionLabel(blockType) {
|
|
5722
5699
|
return blockType === "script" ? "transform" : blockType;
|
|
5723
5700
|
}
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
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);
|
|
5736
5728
|
// #2644 criterion 1 — the `[test]` key set comes from the vendored
|
|
5737
5729
|
// definition, so a field added there is emitted by pull with no edit here.
|
|
5738
5730
|
// The three reference fields carry the resolved NAME (a test file is portable
|
|
5739
5731
|
// across apps); the rest keep the empty-string / "{}" / "[]" spellings a test
|
|
5740
5732
|
// file has always used for "not set".
|
|
5741
|
-
const
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
}
|
|
5766
|
-
|
|
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 });
|
|
5767
5776
|
}
|
|
5768
5777
|
/**
|
|
5769
5778
|
* The TOML spelling of a JSON-valued test-case field (#2769).
|
|
@@ -5813,20 +5822,22 @@ export function parseTestCaseToml(tomlData) {
|
|
|
5813
5822
|
description: (value) => (value ? value : undefined),
|
|
5814
5823
|
// Always sent — the server requires it, and `{}` is the empty spelling.
|
|
5815
5824
|
inputVariables: (value) => parseJsonText(value, ["{}"]) ?? {},
|
|
5816
|
-
//
|
|
5817
|
-
//
|
|
5818
|
-
//
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
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,
|
|
5822
5834
|
expectedOutputPattern: (value) => (value ? value : undefined),
|
|
5823
5835
|
expectedOutputContains: (value) => parseJsonText(value, ["[]"]),
|
|
5824
5836
|
expectedJsonSubset: (value) => parseJsonText(value, ["{}"]),
|
|
5825
5837
|
},
|
|
5826
5838
|
});
|
|
5827
5839
|
// An omitted field must be absent from the body, not present-and-undefined:
|
|
5828
|
-
// the
|
|
5829
|
-
// check reads the body's keys.
|
|
5840
|
+
// the server's unknown-key check reads the body's keys.
|
|
5830
5841
|
for (const key of Object.keys(payload)) {
|
|
5831
5842
|
if (payload[key] === undefined)
|
|
5832
5843
|
delete payload[key];
|
|
@@ -6179,6 +6190,35 @@ export async function pullFunctions(client, appId, configDir, logger = () => { }
|
|
|
6179
6190
|
items,
|
|
6180
6191
|
};
|
|
6181
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
|
+
}
|
|
6182
6222
|
/**
|
|
6183
6223
|
* Every test case a block has, draining the cursor (#2769).
|
|
6184
6224
|
*
|
|
@@ -6231,6 +6271,27 @@ export function carryForwardTestCaseEntities(params) {
|
|
|
6231
6271
|
}
|
|
6232
6272
|
return carried;
|
|
6233
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
|
+
}
|
|
6234
6295
|
/**
|
|
6235
6296
|
* Write one block's test cases into its `<key>.tests/` sidecar and record them
|
|
6236
6297
|
* in sync state. Exported so the unit tests can drive it against a stubbed
|
|
@@ -6246,10 +6307,9 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6246
6307
|
const testsDir = getTestsDir(configDir, blockType, blockKey);
|
|
6247
6308
|
// A block with no cases and no sidecar is not a block whose sidecar is empty:
|
|
6248
6309
|
// creating the directory here would commit an empty folder Git cannot carry.
|
|
6249
|
-
if (testCases.length === 0 && !existsSync(testsDir))
|
|
6250
|
-
return { ok: true, count: 0 };
|
|
6251
|
-
|
|
6252
|
-
ensureDir(testsDir);
|
|
6310
|
+
if (testCases.length === 0 && !existsSync(testsDir)) {
|
|
6311
|
+
return { ok: true, count: 0, unwritten: [] };
|
|
6312
|
+
}
|
|
6253
6313
|
const pulledSlugs = new Set();
|
|
6254
6314
|
// #2896 — a keyed case is written to the file its key names, so pull stops
|
|
6255
6315
|
// renaming what push authored: the repro's `full-refresh-takes-all-history`
|
|
@@ -6259,15 +6319,35 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6259
6319
|
// The key is server data becoming a path: the containment check an
|
|
6260
6320
|
// attachment's filename gets, so nothing lands outside the sidecar dir.
|
|
6261
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 = [];
|
|
6262
6328
|
for (const tc of testCases) {
|
|
6263
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) {
|
|
6264
6347
|
pulledSlugs.add(slug);
|
|
6265
6348
|
// Write test case TOML
|
|
6266
6349
|
const tomlPath = join(testsDir, `${slug}.toml`);
|
|
6267
|
-
writeFileSync(tomlPath,
|
|
6268
|
-
file: tomlPath,
|
|
6269
|
-
logger: (message) => log(message),
|
|
6270
|
-
}));
|
|
6350
|
+
writeFileSync(tomlPath, toml);
|
|
6271
6351
|
// Attachments: the sidecar directory is the authored set, so pull makes it
|
|
6272
6352
|
// match the server — downloading what is there, recording each file's
|
|
6273
6353
|
// content hash (so push can see a byte change under an unchanged name), and
|
|
@@ -6359,12 +6439,79 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6359
6439
|
}),
|
|
6360
6440
|
};
|
|
6361
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
|
+
}
|
|
6362
6505
|
// Clean up local files for remotely-deleted test cases
|
|
6363
6506
|
if (existsSync(testsDir)) {
|
|
6364
6507
|
const localFiles = readdirSync(testsDir).filter((f) => f.endsWith(".toml"));
|
|
6365
6508
|
for (const file of localFiles) {
|
|
6366
6509
|
const localSlug = basename(file, ".toml");
|
|
6367
|
-
|
|
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) {
|
|
6368
6515
|
// Remove orphaned TOML
|
|
6369
6516
|
unlinkSync(join(testsDir, file));
|
|
6370
6517
|
// Remove orphaned attachment dir if exists
|
|
@@ -6384,7 +6531,7 @@ export async function pullTestCasesForBlock(params) {
|
|
|
6384
6531
|
}
|
|
6385
6532
|
}
|
|
6386
6533
|
}
|
|
6387
|
-
return { ok: true, count:
|
|
6534
|
+
return { ok: true, count: writable.length, unwritten };
|
|
6388
6535
|
}
|
|
6389
6536
|
/**
|
|
6390
6537
|
* One pull leg: every block of a type the pull selected (#2769).
|
|
@@ -6398,6 +6545,7 @@ export async function pullTestCasesForBlocks(params) {
|
|
|
6398
6545
|
const log = params.logger ?? ((message) => info(message));
|
|
6399
6546
|
let count = 0;
|
|
6400
6547
|
const skippedBlocks = [];
|
|
6548
|
+
const unwritten = [];
|
|
6401
6549
|
for (const block of params.blocks) {
|
|
6402
6550
|
const outcome = await pullTestCasesForBlock({
|
|
6403
6551
|
client: params.client,
|
|
@@ -6425,9 +6573,14 @@ export async function pullTestCasesForBlocks(params) {
|
|
|
6425
6573
|
if (outcome.count > 0) {
|
|
6426
6574
|
log(` Wrote ${outcome.count} test case(s) for ${params.blockType}: ${block.key}`);
|
|
6427
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
|
+
}
|
|
6428
6580
|
count += outcome.count;
|
|
6581
|
+
unwritten.push(...outcome.unwritten);
|
|
6429
6582
|
}
|
|
6430
|
-
return { count, skippedBlocks };
|
|
6583
|
+
return { count, skippedBlocks, unwritten };
|
|
6431
6584
|
}
|
|
6432
6585
|
/** The 10 MB per-attachment cap the server enforces. */
|
|
6433
6586
|
const TEN_MB = 10 * 1024 * 1024;
|
|
@@ -6619,9 +6772,20 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6619
6772
|
continue;
|
|
6620
6773
|
}
|
|
6621
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
|
+
}
|
|
6622
6786
|
const testPayload = parseTestCaseToml(tomlData);
|
|
6623
|
-
// Resolve
|
|
6624
|
-
// 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
|
|
6625
6789
|
// no pin, quietly changing what the case tests (#2769). The exception is a
|
|
6626
6790
|
// preview of a block this push has not created yet — its configs cannot
|
|
6627
6791
|
// exist to be named, so a dry run reports the reference as pending rather
|
|
@@ -6642,7 +6806,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6642
6806
|
delete testPayload._configName;
|
|
6643
6807
|
delete testPayload._evaluatorPromptKey;
|
|
6644
6808
|
delete testPayload._evaluatorConfigName;
|
|
6645
|
-
if (configName
|
|
6809
|
+
if (configName) {
|
|
6646
6810
|
const configMap = blockType === "prompt"
|
|
6647
6811
|
? resolutionMaps?.promptConfigNameToId
|
|
6648
6812
|
: blockType === "script"
|
|
@@ -6660,14 +6824,14 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6660
6824
|
else
|
|
6661
6825
|
noteUnresolved(`configName "${configName}"`, blockType, blockKey);
|
|
6662
6826
|
}
|
|
6663
|
-
if (evaluatorPromptKey
|
|
6827
|
+
if (evaluatorPromptKey) {
|
|
6664
6828
|
const resolvedId = resolutionMaps?.promptKeyToId.get(evaluatorPromptKey);
|
|
6665
6829
|
if (resolvedId)
|
|
6666
6830
|
testPayload.evaluatorPromptId = resolvedId;
|
|
6667
6831
|
else
|
|
6668
6832
|
noteUnresolved(`evaluatorPromptKey "${evaluatorPromptKey}"`, "prompt", evaluatorPromptKey);
|
|
6669
6833
|
}
|
|
6670
|
-
if (evaluatorConfigName
|
|
6834
|
+
if (evaluatorConfigName) {
|
|
6671
6835
|
let resolvedId = evaluatorPromptKey
|
|
6672
6836
|
? resolutionMaps?.promptConfigNameToId.get(`${evaluatorPromptKey}#${evaluatorConfigName}`)
|
|
6673
6837
|
: undefined;
|
|
@@ -7248,9 +7412,10 @@ export function pairTestCases(input) {
|
|
|
7248
7412
|
* integration and workflow sidecar pinned to a config reported Modified
|
|
7249
7413
|
* forever, and push read the same pair as server drift.
|
|
7250
7414
|
*
|
|
7251
|
-
* One listing per block that has sidecars, and a failed one is silent:
|
|
7252
|
-
* unresolved reference compares as
|
|
7253
|
-
*
|
|
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.
|
|
7254
7419
|
*/
|
|
7255
7420
|
async function fillBlockConfigNames(client, appId, blockType, blockId, configIdToName) {
|
|
7256
7421
|
try {
|
|
@@ -7308,10 +7473,6 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7308
7473
|
});
|
|
7309
7474
|
for (const slug of localSlugs) {
|
|
7310
7475
|
const live = paired.bySlug.get(slug);
|
|
7311
|
-
if (!live) {
|
|
7312
|
-
rows.push({ ...base, slug, status: LOCAL_ONLY_NEW });
|
|
7313
|
-
continue;
|
|
7314
|
-
}
|
|
7315
7476
|
const filePath = join(testsDir, `${slug}.toml`);
|
|
7316
7477
|
let row;
|
|
7317
7478
|
try {
|
|
@@ -7319,7 +7480,9 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7319
7480
|
// #2880 criterion 1 — a sidecar push's preflight would reject is not one
|
|
7320
7481
|
// this command calls synced, and the message is the same one. The WHOLE
|
|
7321
7482
|
// preflight, not just the sidecar's own rules: an unknown key or a
|
|
7322
|
-
// 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.
|
|
7323
7486
|
const errors = testCaseSidecarPreflightErrors(filePath, tomlData);
|
|
7324
7487
|
if (errors.length > 0) {
|
|
7325
7488
|
row = {
|
|
@@ -7329,6 +7492,10 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7329
7492
|
hint: errors.join("; "),
|
|
7330
7493
|
};
|
|
7331
7494
|
}
|
|
7495
|
+
else if (!live) {
|
|
7496
|
+
rows.push({ ...base, slug, status: LOCAL_ONLY_NEW });
|
|
7497
|
+
continue;
|
|
7498
|
+
}
|
|
7332
7499
|
else {
|
|
7333
7500
|
const verdict = compareLocalToRemote(spec, tomlData, live, { ruleSetIdToName: new Map(), ruleSetNameToId: new Map() }, extra,
|
|
7334
7501
|
// Both sides read a reference through the same lookups — see
|
|
@@ -7345,6 +7512,12 @@ export async function compareTestCasesForBlock(params) {
|
|
|
7345
7512
|
hint: `${filePath}: ${err?.message || String(err)}`,
|
|
7346
7513
|
};
|
|
7347
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
|
+
}
|
|
7348
7521
|
// #2880 behavior 19 — attachments are part of what push would do, read
|
|
7349
7522
|
// from the SAME plan push executes. A sidecar whose text compares clean but
|
|
7350
7523
|
// whose fixtures have not been uploaded is not Synced.
|
|
@@ -7820,7 +7993,11 @@ export function registerConfigSyncCommands(sync) {
|
|
|
7820
7993
|
const appWorkflowItems = selectPull("workflow", workflowItems.filter((w) => !isPlatformOwnedWorkflow(w)), (w) => w.workflowKey);
|
|
7821
7994
|
// Fetch details for each entity
|
|
7822
7995
|
const integrations = selectPull("integration", await Promise.all(integrationItems.map((i) => client.getIntegration(resolvedAppId, i.integrationId))), (i) => i.integrationKey);
|
|
7823
|
-
|
|
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);
|
|
7824
8001
|
const workflows = await Promise.all(appWorkflowItems.map(async (w) => {
|
|
7825
8002
|
const workflowData = await client.getWorkflow(resolvedAppId, w.workflowId);
|
|
7826
8003
|
// #2645 — fetch EVERY config's body, not just the active one. The
|
|
@@ -8321,57 +8498,38 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8321
8498
|
if (pulledFunctionsCount > 0) {
|
|
8322
8499
|
info(` Pulled ${pulledFunctionsCount} function(s)`);
|
|
8323
8500
|
}
|
|
8324
|
-
//
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
// Also include workflow config names
|
|
8336
|
-
for (const { configs } of workflows) {
|
|
8337
|
-
if (configs) {
|
|
8338
|
-
for (const config of configs) {
|
|
8339
|
-
configIdToName.set(config.configId, config.configName);
|
|
8340
|
-
}
|
|
8341
|
-
}
|
|
8342
|
-
}
|
|
8343
|
-
// Include script config names so a pinned script test case
|
|
8344
|
-
// round-trips its `configName` reference (the same way prompt/workflow
|
|
8345
|
-
// configs above do). One extra `configs` fetch per pulled script.
|
|
8346
|
-
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)) {
|
|
8347
8512
|
try {
|
|
8348
|
-
const { items
|
|
8349
|
-
|
|
8350
|
-
configIdToName.set(config.configId, config.configName);
|
|
8351
|
-
}
|
|
8513
|
+
const { items } = await client.listScriptConfigs(resolvedAppId, entity.id);
|
|
8514
|
+
blockConfigs.push(...items);
|
|
8352
8515
|
}
|
|
8353
8516
|
catch {
|
|
8354
|
-
// Non-fatal
|
|
8355
|
-
// its legacy id spelling instead of a name (#2769), so the pin
|
|
8356
|
-
// survives. The header/body still pulled fine — `name` retained.
|
|
8357
|
-
void name;
|
|
8517
|
+
// Non-fatal — see above.
|
|
8358
8518
|
}
|
|
8359
8519
|
}
|
|
8360
|
-
// Integration config names, for the same reason (#2769). A failed fetch
|
|
8361
|
-
// is non-fatal here too: `serializeTestCase` keeps the pin as `configId`
|
|
8362
|
-
// rather than writing an empty reference.
|
|
8363
8520
|
for (const integration of integrations) {
|
|
8364
8521
|
try {
|
|
8365
|
-
const { items
|
|
8366
|
-
|
|
8367
|
-
configIdToName.set(config.configId, config.configName);
|
|
8368
|
-
}
|
|
8522
|
+
const { items } = await client.listIntegrationConfigs(resolvedAppId, integration.integrationId);
|
|
8523
|
+
blockConfigs.push(...items);
|
|
8369
8524
|
}
|
|
8370
8525
|
catch {
|
|
8371
8526
|
// Non-fatal — see above.
|
|
8372
8527
|
}
|
|
8373
8528
|
}
|
|
8374
|
-
const testCaseLookupMaps = {
|
|
8529
|
+
const testCaseLookupMaps = testCaseLookupMapsForPull({
|
|
8530
|
+
prompts: promptDetails,
|
|
8531
|
+
configs: blockConfigs,
|
|
8532
|
+
});
|
|
8375
8533
|
// #2731 B4 — pull is one of the two moments local and server provably
|
|
8376
8534
|
// agree, so it stamps the semantic baseline beside the byte hash (push
|
|
8377
8535
|
// stamps the other one, from the entity the server returned). The
|
|
@@ -8411,6 +8569,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8411
8569
|
const testCaseEntities = {};
|
|
8412
8570
|
const priorTestCaseEntities = priorEntities.testCases;
|
|
8413
8571
|
let totalTestCases = 0;
|
|
8572
|
+
let unwrittenTestCases = 0;
|
|
8414
8573
|
for (const [blockType, blocks] of [
|
|
8415
8574
|
[
|
|
8416
8575
|
"prompt",
|
|
@@ -8440,7 +8599,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8440
8599
|
})),
|
|
8441
8600
|
],
|
|
8442
8601
|
]) {
|
|
8443
|
-
const { count } = await pullTestCasesForBlocks({
|
|
8602
|
+
const { count, unwritten } = await pullTestCasesForBlocks({
|
|
8444
8603
|
client,
|
|
8445
8604
|
appId: resolvedAppId,
|
|
8446
8605
|
configDir,
|
|
@@ -8452,6 +8611,14 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8452
8611
|
logger: (message) => info(message),
|
|
8453
8612
|
});
|
|
8454
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`);
|
|
8455
8622
|
}
|
|
8456
8623
|
// Build ruleSetId → name map for database types and group type configs
|
|
8457
8624
|
const ruleSets = selectPull("rule-set", Array.isArray(ruleSetsResult) ? ruleSetsResult : [], (rs) => ruleSetFileKey(rs));
|
|
@@ -8969,6 +9136,9 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8969
9136
|
keyValue("Functions", pulledFunctionsCount);
|
|
8970
9137
|
keyValue("Email Templates", emailTemplates.length);
|
|
8971
9138
|
keyValue("Test Cases", totalTestCases);
|
|
9139
|
+
if (unwrittenTestCases > 0) {
|
|
9140
|
+
keyValue("Test Cases not written", unwrittenTestCases);
|
|
9141
|
+
}
|
|
8972
9142
|
keyValue("Database Types", databaseTypesWithOps.length);
|
|
8973
9143
|
keyValue("Rule Sets", ruleSets.length);
|
|
8974
9144
|
keyValue("Group Type Configs", groupTypeConfigs.length);
|
|
@@ -17263,28 +17433,28 @@ What a push guarantees (two stages):
|
|
|
17263
17433
|
let unchangedCount = 0;
|
|
17264
17434
|
// ---- database-type-configs/ pass ----
|
|
17265
17435
|
if (dbFiles.length > 0) {
|
|
17266
|
-
//
|
|
17267
|
-
//
|
|
17268
|
-
//
|
|
17269
|
-
//
|
|
17270
|
-
//
|
|
17271
|
-
//
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
const migrateSyncState = loadSyncState(configDir);
|
|
17278
|
-
if (migrateSyncState?.entities?.ruleSets) {
|
|
17279
|
-
for (const [fileKey, entry] of Object.entries(migrateSyncState.entities.ruleSets)) {
|
|
17280
|
-
if (entry && typeof entry === "object" && "id" in entry && entry.id) {
|
|
17281
|
-
// fileKey is the sanitized rule-set name (see config pull at
|
|
17282
|
-
// sync.ts:1385). It's the same shape the server returned and
|
|
17283
|
-
// matches what a TOML file's ruleSetName field would carry.
|
|
17284
|
-
ruleSetIdToName.set(entry.id, fileKey);
|
|
17285
|
-
}
|
|
17286
|
-
}
|
|
17436
|
+
// #3997 — refuse a tree carrying a retired key (the id-based
|
|
17437
|
+
// `ruleSetId`) with the message `config push` gives, before anything is
|
|
17438
|
+
// rewritten. This is a separate pass, not a check inside the loop below:
|
|
17439
|
+
// that loop writes each file and saves sync state before it parses the
|
|
17440
|
+
// next, so an in-loop abort would leave earlier files rewritten. Every
|
|
17441
|
+
// offending file is reported, so the author fixes the tree once.
|
|
17442
|
+
const retiredKeyErrors = [];
|
|
17443
|
+
for (const file of dbFiles) {
|
|
17444
|
+
const message = configUnknownKeyError(`database-type-configs/${file}`, parseTomlFile(join(dbTypesDir, file)), DATABASE_TYPE_TABLE);
|
|
17445
|
+
if (message)
|
|
17446
|
+
retiredKeyErrors.push(message);
|
|
17287
17447
|
}
|
|
17448
|
+
if (retiredKeyErrors.length > 0) {
|
|
17449
|
+
for (const message of retiredKeyErrors)
|
|
17450
|
+
error(` ${message}`);
|
|
17451
|
+
error(`Aborting migrate-toml: ${retiredKeyErrors.length} database-type ` +
|
|
17452
|
+
"file(s) carry a retired or unrecognized key; nothing was rewritten.");
|
|
17453
|
+
process.exit(1);
|
|
17454
|
+
}
|
|
17455
|
+
// The parser yields the rule-set reference only as `_ruleSetName`,
|
|
17456
|
+
// which the serializer prefers, so there is no id to map to a name.
|
|
17457
|
+
const ruleSetIdToName = new Map();
|
|
17288
17458
|
for (const file of dbFiles) {
|
|
17289
17459
|
const filePath = join(dbTypesDir, file);
|
|
17290
17460
|
const rawBefore = readFileSync(filePath, "utf-8");
|