@intentius/chant-lexicon-fly 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@
26
26
  * real-Fly target flyApply writes. Secrets are apply-only (D7): flaps returns
27
27
  * only a digest, so they never enter a diff and are excluded here.
28
28
  */
29
- import type { ResourceMetadata } from "@intentius/chant/lexicon";
29
+ import type { ObservationResult, ResourceMetadata } from "@intentius/chant/lexicon";
30
30
  import { type ChangeSet } from "@intentius/chant/lifecycle/change-set";
31
31
  import { type FlyHttp } from "./op/activities/fly-apply.js";
32
32
  export interface DescribeResourcesOptions {
@@ -51,7 +51,7 @@ export interface DescribeResourcesOptions {
51
51
  * `http` is injectable for tests; the default reuses the applier's fetch client
52
52
  * (bearer token from FLY_API_TOKEN).
53
53
  */
54
- export declare function describeResources(options: DescribeResourcesOptions, http?: FlyHttp, signal?: AbortSignal): Promise<Record<string, ResourceMetadata>>;
54
+ export declare function describeResources(options: DescribeResourcesOptions, http?: FlyHttp, signal?: AbortSignal): Promise<ObservationResult>;
55
55
  /**
56
56
  * Read-only plan (#767 §4): a `flyApply` dry-run. Reads live state via
57
57
  * {@link describeResources}, promotes it to core's typed change set, then
@@ -1 +1 @@
1
- {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAqBL,KAAK,OAAO,EAIb,MAAM,2BAA2B,CAAC;AAcnC,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAeD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CA0M3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,wBAAwB,GAAG;IAClC,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACjD,EACD,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BrD"}
1
+ {"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAoB,MAAM,0BAA0B,CAAC;AAEtG,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAqBL,KAAK,OAAO,EAIb,MAAM,2BAA2B,CAAC;AAcnC,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAeD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,iBAAiB,CAAC,CA4R5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,wBAAwB,GAAG;IAClC,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACjD,EACD,IAAI,GAAE,OAA0B,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAkCrD"}
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "8097252bf7eb52757c71e36712881bf6875fb6ee8402720d09e76fc610673b2f",
4
+ "manifest.json": "90960c725f3d0ece08fdfa0ef23e5d796094f41c4ee9ead658e6657d53ab7e1e",
5
5
  "meta.json": "e46e65ba7544fafc6dbb3fd48b841904b5ae6f7cb4d517a940ed68b01626c097",
6
6
  "types/index.d.ts": "07f5d04d0d3831cae053b1f0f5dd661c58e9708f45067a0fc85dd94079ac144b"
7
7
  },
8
- "composite": "3a126dd75324cc55919dbdd7ab7ef3eec0c1db50115daa0d8a702d5e6570cb1c"
8
+ "composite": "f1bba49b606cb7bb617816ebfb6a0cfe0c9a8e04478bb27a93cd2f426d69abf4"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fly",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fly"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fly",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -57,7 +57,7 @@
57
57
  "typescript": "^5.9.3"
58
58
  },
59
59
  "peerDependencies": {
60
- "@intentius/chant": "^0.28.0",
60
+ "@intentius/chant": "^0.30.0",
61
61
  "typescript": "^5.9.3"
62
62
  },
63
63
  "description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
@@ -1,4 +1,5 @@
1
1
  import { describe, test, expect } from "vitest";
2
+ import { describeObservationConformance } from "@intentius/chant-test-utils";
2
3
  import { describeResources, flyPlan } from "./describe-resources";
3
4
  import type { FlyHttp } from "./op/activities/fly-apply";
4
5
 
@@ -72,21 +73,21 @@ describe("describeResources: live machines → ResourceMetadata verdicts", () =>
72
73
  );
73
74
 
74
75
  // Declared machine keyed by its entity name; orphan keyed by its live name.
75
- expect(res.web.type).toBe("Fly::Machines::Machine");
76
- expect(res.web.ownership).toBe("owned");
77
- expect(res.web.physicalId).toBe("m-web");
78
- expect(res.web.status).toBe("started");
79
- expect((res.web.attributes as { config?: unknown }).config).toEqual({
76
+ expect(res.resources.web.type).toBe("Fly::Machines::Machine");
77
+ expect(res.resources.web.ownership).toBe("owned");
78
+ expect(res.resources.web.physicalId).toBe("m-web");
79
+ expect(res.resources.web.status).toBe("started");
80
+ expect((res.resources.web.attributes as { config?: unknown }).config).toEqual({
80
81
  image: "nginx:1",
81
82
  metadata: { "managed-by": "chant" },
82
83
  });
83
84
 
84
- expect(res.legacy.ownership).toBe("foreign");
85
- expect(res.legacy.type).toBe("Fly::Machines::Machine");
85
+ expect(res.resources.legacy.ownership).toBe("foreign");
86
+ expect(res.resources.legacy.type).toBe("Fly::Machines::Machine");
86
87
 
87
88
  // App surfaced as owned (app-boundary: it carries an owned machine).
88
- expect(res.app.type).toBe("Fly::Machines::App");
89
- expect(res.app.ownership).toBe("owned");
89
+ expect(res.resources.app.type).toBe("Fly::Machines::App");
90
+ expect(res.resources.app.ownership).toBe("owned");
90
91
  });
91
92
 
92
93
  test("the owned filter drops foreign entries", async () => {
@@ -98,8 +99,8 @@ describe("describeResources: live machines → ResourceMetadata verdicts", () =>
98
99
  { environment: "prod", buildOutput: PLAN, entityNames: ["app", "web"], entities: ENTS, owned: true, endpoint: ENDPOINT },
99
100
  http,
100
101
  );
101
- expect(res.web?.ownership).toBe("owned");
102
- expect(res.legacy).toBeUndefined(); // foreign machine filtered out
102
+ expect(res.resources.web?.ownership).toBe("owned");
103
+ expect(res.resources.legacy).toBeUndefined(); // foreign machine filtered out
103
104
  });
104
105
 
105
106
  test("terminal machines (destroyed/destroying) are not reported live", async () => {
@@ -111,8 +112,8 @@ describe("describeResources: live machines → ResourceMetadata verdicts", () =>
111
112
  { environment: "prod", buildOutput: PLAN, entityNames: ["app", "web"], entities: ENTS, endpoint: ENDPOINT },
112
113
  http,
113
114
  );
114
- expect(res.web).toBeDefined();
115
- expect(res.gone).toBeUndefined();
115
+ expect(res.resources.web).toBeDefined();
116
+ expect(res.resources.gone).toBeUndefined();
116
117
  });
117
118
 
118
119
  test("empty build output returns nothing", async () => {
@@ -120,7 +121,27 @@ describe("describeResources: live machines → ResourceMetadata verdicts", () =>
120
121
  { environment: "prod", buildOutput: "", entityNames: [], entities: new Map(), endpoint: ENDPOINT },
121
122
  fakeHttp([]),
122
123
  );
123
- expect(res).toEqual({});
124
+ expect(res.resources).toEqual({});
125
+ });
126
+
127
+ test("with no readable plan, declared entities are unobserved rather than absent (#1089)", async () => {
128
+ const res = await describeResources(
129
+ { environment: "prod", buildOutput: "", entityNames: ["web"], entities: ENTS, endpoint: ENDPOINT },
130
+ fakeHttp([]),
131
+ );
132
+ expect(res.resources).toEqual({});
133
+ expect(res.unobserved?.web?.reason).toBe("read-failed");
134
+ expect(res.unobserved?.app?.reason).toBe("read-failed");
135
+ });
136
+
137
+ test("the owned filter records a withheld declared machine as `filtered`, not absent (#1089)", async () => {
138
+ const http = fakeHttp([liveMachine("web", "nginx:1", { id: "m-web", owned: false })]);
139
+ const res = await describeResources(
140
+ { environment: "prod", buildOutput: PLAN, entityNames: ["app", "web"], entities: ENTS, owned: true, endpoint: ENDPOINT },
141
+ http,
142
+ );
143
+ expect(res.resources.web).toBeUndefined();
144
+ expect(res.unobserved?.web?.reason).toBe("filtered");
124
145
  });
125
146
  });
126
147
 
@@ -185,4 +206,56 @@ describe("flyPlan: declared-vs-live change set (create/noop/delete/adopt)", () =
185
206
  );
186
207
  expect(changeSet.entries.find((e) => e.name === "web")?.action).toBe("update");
187
208
  });
209
+
210
+ test("an unreadable plan makes every declared entity unobserved, not a create (#1089)", async () => {
211
+ const { changeSet } = await flyPlan(
212
+ { environment: "prod", buildOutput: "not json", entityNames: [...ENTS.keys()], entities: ENTS, endpoint: ENDPOINT },
213
+ fakeHttp([]),
214
+ );
215
+ expect(changeSet.entries.map((e) => e.action)).toEqual(["unobserved", "unobserved"]);
216
+ });
217
+ });
218
+
219
+ // The shared conformance suite (#1089).
220
+ describeObservationConformance({
221
+ lexicon: "fly",
222
+ scenarios: [
223
+ {
224
+ name: "a build output that is not a readable fly plan",
225
+ declared: ["app", "web"],
226
+ expectUnobserved: ["app", "web"],
227
+ run: () =>
228
+ describeResources(
229
+ { environment: "prod", buildOutput: "{{ not json", entityNames: ["app", "web"], entities: ENTS, endpoint: ENDPOINT },
230
+ fakeHttp([]),
231
+ ),
232
+ },
233
+ {
234
+ name: "the owned filter withholding a declared machine",
235
+ declared: ["app", "web"],
236
+ expectUnobserved: ["web"],
237
+ run: () =>
238
+ describeResources(
239
+ {
240
+ environment: "prod",
241
+ buildOutput: PLAN,
242
+ entityNames: ["app", "web"],
243
+ entities: ENTS,
244
+ owned: true,
245
+ endpoint: ENDPOINT,
246
+ },
247
+ fakeHttp([liveMachine("web", "nginx:1", { owned: false })]),
248
+ ),
249
+ },
250
+ {
251
+ name: "a healthy read",
252
+ declared: ["app", "web"],
253
+ expectPresent: ["app", "web"],
254
+ run: () =>
255
+ describeResources(
256
+ { environment: "prod", buildOutput: PLAN, entityNames: ["app", "web"], entities: ENTS, endpoint: ENDPOINT },
257
+ fakeHttp([liveMachine("web", "nginx:1")]),
258
+ ),
259
+ },
260
+ ],
188
261
  });
Binary file
@@ -77,15 +77,15 @@ describe("describeResources against live mudflaps (#767)", () => {
77
77
 
78
78
  // 3. describeResources sees web owned, legacy foreign.
79
79
  const live = await describeResources({ environment: "it", buildOutput, entityNames: [...ents.keys()], entities: ents, endpoint });
80
- expect(live.web?.ownership).toBe("owned");
81
- expect(live.web?.status).toBe("started");
82
- expect(live.legacy?.ownership).toBe("foreign");
83
- expect(live.app?.ownership).toBe("owned"); // app-boundary: carries an owned machine
80
+ expect(live.resources.web?.ownership).toBe("owned");
81
+ expect(live.resources.web?.status).toBe("started");
82
+ expect(live.resources.legacy?.ownership).toBe("foreign");
83
+ expect(live.resources.app?.ownership).toBe("owned"); // app-boundary: carries an owned machine
84
84
 
85
85
  // 4. The owned filter drops the unmarked machine.
86
86
  const ownedOnly = await describeResources({ environment: "it", buildOutput, entityNames: [...ents.keys()], entities: ents, owned: true, endpoint });
87
- expect(ownedOnly.web?.ownership).toBe("owned");
88
- expect(ownedOnly.legacy).toBeUndefined();
87
+ expect(ownedOnly.resources.web?.ownership).toBe("owned");
88
+ expect(ownedOnly.resources.legacy).toBeUndefined();
89
89
 
90
90
  // 5. Drop "web" from the declared plan. The plan now classifies the live
91
91
  // owned "web" as a delete; the unmarked "legacy" stays adopt (never delete).