@intentius/chant-lexicon-azure 0.15.0 → 0.15.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "3b3b9ea67bcddf7d194ad6885c0ef2b41fe820dcc229b36f0593aee5b3227606",
4
+ "manifest.json": "9b6eec8e49d61507ee2c5451aac2dd7dd84d0a929c5955898a70caaace370115",
5
5
  "meta.json": "4b9c0be734f8b4a5fbba3136e14aaa1ea73ff0ee93f2aa480db83e476ce727b5",
6
6
  "types/index.d.ts": "d62c0fd7947ddf04a9126cc8bf44a9f4437f21cf8647fc812c022398f2c00d57",
7
7
  "rules/hardcoded-location.ts": "a9b1d1cec93f2ca9c5206641f53fc9621cefcfd2c00b3ab89c194b30a75fa949",
@@ -33,5 +33,5 @@
33
33
  "skills/chant-azure-patterns.md": "1a3bacfac612826b77332d2692a59195638db9f1b5e8ea2eda7579621d25f603",
34
34
  "skills/chant-azure-aks.md": "2d4e0098c1a22b54ffadd410564d9df0f3a04cb4eb7c6261f20ae33106e7aca8"
35
35
  },
36
- "composite": "18a72855f694f2b11534c8dafffcd84b8fa040f8e9fc47285135488a7c357639"
36
+ "composite": "4b88a0517fc5aa7720d15b6e8cce2ffac5986819d42ddd3e5832135157c4981b"
37
37
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azure",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Azure",
6
6
  "intrinsics": [
@@ -1,9 +1,3 @@
1
- /** Context for evaluating the ARM template expressions chant emits. */
2
- export interface ArmContext {
3
- subscriptionId: string;
4
- resourceGroup: string;
5
- location: string;
6
- }
7
1
  /** One ARM resource from a `deploymentTemplate.json` `resources[]`. */
8
2
  export interface ArmResource {
9
3
  type: string;
@@ -14,25 +8,44 @@ export interface ArmResource {
14
8
  sku?: unknown;
15
9
  kind?: unknown;
16
10
  tags?: Record<string, string>;
11
+ dependsOn?: unknown;
17
12
  }
18
- /**
19
- * Evaluate an ARM template expression string (`"[...]"`). Supports the functions
20
- * chant's serializer emits: `concat`, `uniqueString`, `resourceGroup()` (`.id` /
21
- * `.location`), `subscription()` (`.subscriptionId`), and string literals. A
22
- * non-expression string is returned unchanged. Pure.
23
- */
24
- export declare function evalArmString(s: string, ctx: ArmContext): string;
25
- /** Recursively evaluate every string in a value against the ARM context. Pure. */
26
- export declare function evalArm(value: unknown, ctx: ArmContext): unknown;
27
13
  /** Injectable HTTP client — mirrors the GCP applier so tests avoid the network. */
28
14
  export type AzHttp = (method: string, url: string, body?: unknown, signal?: AbortSignal) => Promise<{
29
15
  status: number;
30
16
  text: string;
31
17
  }>;
32
- /** The ARM resource-ID PUT URL for a resource under a resource group. Pure. */
33
- export declare function armResourceUrl(resource: ArmResource, ctx: ArmContext, base: string): string;
34
- /** The ARM resource PUT body (location/properties/sku/kind/tags), expressions evaluated. Pure. */
35
- export declare function armResourceBody(resource: ArmResource, ctx: ArmContext): Record<string, unknown>;
18
+ /**
19
+ * Context for evaluating ARM template expressions. `deployed` holds the response
20
+ * bodies of resources already applied this run (keyed by evaluated name) so
21
+ * `reference()` resolves; `http`/`base` let `listKeys()` call the resource's
22
+ * key action.
23
+ */
24
+ export interface ArmEvalCtx {
25
+ subscriptionId: string;
26
+ resourceGroup: string;
27
+ location: string;
28
+ deployed: Map<string, unknown>;
29
+ http: AzHttp;
30
+ base: string;
31
+ signal?: AbortSignal;
32
+ }
33
+ /** Evaluate an ARM expression string (`"[...]"`); a plain string is returned as-is. */
34
+ export declare function evalArmString(s: string, ctx: ArmEvalCtx): Promise<unknown>;
35
+ /** Recursively evaluate every string in a value against the ARM context. */
36
+ export declare function evalArm(value: unknown, ctx: ArmEvalCtx): Promise<unknown>;
37
+ /** Resource names this resource references via `resourceId('type','name')` / `reference('name')`. Pure. */
38
+ export declare function armDependencies(resource: ArmResource, names: Set<string>): string[];
39
+ /**
40
+ * Topologically order ARM resources so a referenced resource is applied before
41
+ * the resource that references it. Names that are expressions are ordered as-is
42
+ * (they don't match a literal reference). Throws on a cycle. Pure.
43
+ */
44
+ export declare function orderArmResources(resources: ArmResource[]): ArmResource[];
45
+ /** The ARM resource-ID PUT URL for a resource (name expression evaluated). */
46
+ export declare function armResourceUrl(resource: ArmResource, ctx: ArmEvalCtx): Promise<string>;
47
+ /** The ARM resource PUT body (location/properties/sku/kind/tags), expressions evaluated. */
48
+ export declare function armResourceBody(resource: ArmResource, ctx: ArmEvalCtx): Promise<Record<string, unknown>>;
36
49
  export interface AzApplyArgs {
37
50
  /** Path to a built ARM template (`deploymentTemplate.json`). */
38
51
  templatePath: string;
@@ -44,19 +57,74 @@ export interface AzApplyArgs {
44
57
  endpoint?: string;
45
58
  /** Subscription id. Default: floci-az's local subscription. */
46
59
  subscriptionId?: string;
60
+ /**
61
+ * Delete chant-owned resources of a templated type that are no longer in the
62
+ * template (owned-only prune). Destructive — off by default. Foreign
63
+ * (non-chant) resources are never touched.
64
+ */
65
+ prune?: boolean;
47
66
  }
48
67
  /**
49
68
  * The native Azure applier — read a built ARM template and PUT each resource
50
- * directly to the ARM resource-CRUD API, resolving ARM expressions first. This
51
- * is the direct-apply path (the Azure twin of `gcpApply`): it targets floci-az's
52
- * ARM resource endpoints which `az deployment` cannot, since floci-az has no
53
- * `Microsoft.Resources/deployments` provider or real Azure by endpoint override.
54
- * The resource group is ensured first. `http` is injectable for tests.
69
+ * directly to the ARM resource-CRUD API, in dependency order, resolving ARM
70
+ * expressions (including `reference()`/`listKeys()` against resources applied
71
+ * earlier this run). The Azure twin of `gcpApply`: it targets floci-az (which
72
+ * `az deployment` can't, floci-az having no deployments provider) or real Azure
73
+ * by endpoint override; the resource group is ensured first.
55
74
  */
56
75
  export declare function azApply(args: AzApplyArgs, signal?: AbortSignal, http?: AzHttp): Promise<{
57
76
  applied: Array<{
58
77
  type: string;
59
78
  name: string;
60
79
  }>;
80
+ pruned: Array<{
81
+ type: string;
82
+ name: string;
83
+ deleted: boolean;
84
+ }>;
85
+ }>;
86
+ /** The ownership tag azApply stamps on every resource it applies. */
87
+ export declare function chantOwnershipTags(): Record<string, string>;
88
+ /** Whether a resource's tags mark it chant-owned. */
89
+ export declare function isChantOwned(tags: Record<string, string> | null | undefined): boolean;
90
+ /** One resource from the ARM resource-group listing. */
91
+ export interface ArmListItem {
92
+ id: string;
93
+ name: string;
94
+ type: string;
95
+ tags?: Record<string, string>;
96
+ }
97
+ /** List the resources in the group via the ARM resource-list endpoint. */
98
+ export declare function listGroupResources(ctx: ArmEvalCtx, http?: AzHttp, signal?: AbortSignal): Promise<ArmListItem[]>;
99
+ /** Idempotently delete one ARM resource by type/name/apiVersion. A 404 means it is already gone. */
100
+ export declare function deleteArmResource(type: string, name: string, apiVersion: string, ctx: ArmEvalCtx, http?: AzHttp, signal?: AbortSignal): Promise<{
101
+ type: string;
102
+ name: string;
103
+ deleted: boolean;
104
+ }>;
105
+ /**
106
+ * Owned-only prune: for each resource type present in the template, delete the
107
+ * chant-owned live resources of that type whose (evaluated) name is not in the
108
+ * template. Scoped to templated types — like the GCP applier — so a type chant
109
+ * isn't managing this run is left alone, and the type's `apiVersion` is taken
110
+ * from the template. Foreign (non-chant) resources are never touched.
111
+ */
112
+ export declare function pruneArmOrphans(desired: ArmResource[], ctx: ArmEvalCtx, http?: AzHttp, signal?: AbortSignal): Promise<Array<{
113
+ type: string;
114
+ name: string;
115
+ deleted: boolean;
116
+ }>>;
117
+ /**
118
+ * The inverse of {@link azApply} — read a built ARM template and delete the
119
+ * resources it declares, in reverse dependency order (a referrer goes before the
120
+ * resource it references). Idempotent: already-absent resources are a no-op. The
121
+ * Azure twin of `gcpDelete`; `http` is injectable for tests.
122
+ */
123
+ export declare function azDelete(args: AzApplyArgs, signal?: AbortSignal, http?: AzHttp): Promise<{
124
+ deleted: Array<{
125
+ type: string;
126
+ name: string;
127
+ deleted: boolean;
128
+ }>;
61
129
  }>;
62
130
  //# sourceMappingURL=az-apply.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"az-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/az-apply.ts"],"names":[],"mappings":"AAQA,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAKhE;AAED,kFAAkF;AAClF,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAShE;AA4FD,mFAAmF;AACnF,MAAM,MAAM,MAAM,GAAG,CACnB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAY/C,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ/F;AAED,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,WAAW,EACjB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,GAAE,MAAoB,GACzB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CA6B7D"}
1
+ {"version":3,"file":"az-apply.d.ts","sourceRoot":"","sources":["../../../src/op/activities/az-apply.ts"],"names":[],"mappings":"AAQA,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,mFAAmF;AACnF,MAAM,MAAM,MAAM,GAAG,CACnB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAc/C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,uFAAuF;AACvF,wBAAsB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAIhF;AAED,4EAA4E;AAC5E,wBAAsB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAS/E;AAgID,2GAA2G;AAC3G,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAenF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAqBzE;AAID,8EAA8E;AAC9E,wBAAsB,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5F;AAED,4FAA4F;AAC5F,wBAAsB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAQ9G;AAED,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,WAAW,EACjB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,GAAE,MAAoB,GACzB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CA8C9H;AAYD,qEAAqE;AACrE,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE3D;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAErF;AAED,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,0EAA0E;AAC1E,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,GAAE,MAAoB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CASlI;AAED,oGAAoG;AACpG,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE,MAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAM3D;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EAAE,EACtB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE,MAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC,CAmBlE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,GAAE,MAAoB,GACzB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAsB/E"}
@@ -0,0 +1,44 @@
1
+ export interface FlociAzUpArgs {
2
+ /** Container name. Default: `chant-floci-az`. */
3
+ name?: string;
4
+ /** Host port mapped to the emulator's `:4577`. Default: `4577`. */
5
+ port?: number;
6
+ /** Image. Default: `floci/floci-az:latest`. */
7
+ image?: string;
8
+ /** Readiness timeout in ms. Default: `60000`. */
9
+ timeoutMs?: number;
10
+ /** Health poll interval in ms. Default: `2000`. */
11
+ intervalMs?: number;
12
+ }
13
+ export interface FlociAzDownArgs {
14
+ /** Container name to remove. Default: `chant-floci-az`. */
15
+ name?: string;
16
+ }
17
+ /** `docker ps -q -f name=<name>` — non-empty stdout means the container is running. */
18
+ export declare function flociAzExistsCommand(name: string): string;
19
+ /** Build the `docker run` command that boots floci-az. */
20
+ export declare function flociAzRunCommand(args: FlociAzUpArgs): string;
21
+ /** Build the `docker rm -f` command. */
22
+ export declare function flociAzRmCommand(name: string): string;
23
+ /** The floci-az health endpoint URL for a host port. */
24
+ export declare function flociAzHealthUrl(port: number): string;
25
+ /** The ARM endpoint URL (what `azApply`'s `endpoint` should point at). */
26
+ export declare function flociAzEndpoint(port: number): string;
27
+ /**
28
+ * Boot a local floci-az (Azure emulator) in Docker and return its ARM endpoint.
29
+ *
30
+ * Idempotent: reuses a running container of the same name. Waits for the health
31
+ * endpoint to answer, then returns `{ endpoint }` for `azApply({ endpoint })`.
32
+ * The typed twin of the AWS `flociUp` — replaces a raw `docker run` shell step so
33
+ * the emulator lifecycle is modeled, not scripted. Uses longInfra profile — 20m
34
+ * timeout, heartbeat every poll (the image may pull).
35
+ */
36
+ export declare function flociAzUp(args: FlociAzUpArgs, signal?: AbortSignal): Promise<{
37
+ endpoint: string;
38
+ }>;
39
+ /**
40
+ * Stop and remove the local floci-az container. A no-op success when the
41
+ * container is already gone. Uses fastIdempotent profile — 5m timeout.
42
+ */
43
+ export declare function flociAzDown(args: FlociAzDownArgs, signal?: AbortSignal): Promise<void>;
44
+ //# sourceMappingURL=floci-az.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floci-az.d.ts","sourceRoot":"","sources":["../../../src/op/activities/floci-az.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAK7D;AAED,wCAAwC;AACxC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA4CxG;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5F"}
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export { azGroupEnsure, azGroupDelete, azGroupEnsureCommand, azGroupDeleteCommand } from "./azure.js";
7
7
  export type { AzGroupEnsureArgs, AzGroupDeleteArgs } from "./azure.js";
8
- export { azApply, evalArm, evalArmString, armResourceUrl, armResourceBody } from "./az-apply.js";
9
- export type { AzApplyArgs, ArmContext, ArmResource, AzHttp } from "./az-apply.js";
8
+ export { flociAzUp, flociAzDown, flociAzRunCommand, flociAzRmCommand, flociAzExistsCommand, flociAzHealthUrl, flociAzEndpoint, } from "./floci-az.js";
9
+ export type { FlociAzUpArgs, FlociAzDownArgs } from "./floci-az.js";
10
+ export { azApply, azDelete, pruneArmOrphans, deleteArmResource, listGroupResources, chantOwnershipTags, isChantOwned, evalArm, evalArmString, armResourceUrl, armResourceBody, armDependencies, orderArmResources, } from "./az-apply.js";
11
+ export type { AzApplyArgs, ArmEvalCtx, ArmResource, ArmListItem, AzHttp } from "./az-apply.js";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9F,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnG,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIpE,OAAO,EACL,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EACL,OAAO,EACP,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,OAAO,EACP,aAAa,EACb,cAAc,EACd,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-azure",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Azure lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -74,6 +74,6 @@
74
74
  "typescript": "^5.9.3"
75
75
  },
76
76
  "peerDependencies": {
77
- "@intentius/chant": "^0.15.0"
77
+ "@intentius/chant": "^0.15.2"
78
78
  }
79
79
  }
@@ -4,53 +4,119 @@ import {
4
4
  evalArm,
5
5
  armResourceUrl,
6
6
  armResourceBody,
7
+ armDependencies,
8
+ orderArmResources,
7
9
  azApply,
8
- type ArmContext,
10
+ azDelete,
11
+ pruneArmOrphans,
12
+ deleteArmResource,
13
+ listGroupResources,
14
+ chantOwnershipTags,
15
+ isChantOwned,
16
+ type ArmEvalCtx,
9
17
  type ArmResource,
10
18
  type AzHttp,
11
19
  } from "./az-apply";
12
20
 
13
- const CTX: ArmContext = {
14
- subscriptionId: "sub-1",
15
- resourceGroup: "chant-rg",
16
- location: "eastus",
17
- };
21
+ const noHttp: AzHttp = async () => ({ status: 200, text: "{}" });
22
+
23
+ function ctx(over: Partial<ArmEvalCtx> = {}): ArmEvalCtx {
24
+ return {
25
+ subscriptionId: "sub-1",
26
+ resourceGroup: "chant-rg",
27
+ location: "eastus",
28
+ deployed: new Map(),
29
+ http: noHttp,
30
+ base: "http://x",
31
+ ...over,
32
+ };
33
+ }
18
34
 
19
- describe("evalArmString (#706)", () => {
20
- test("resourceGroup().location / .id", () => {
21
- expect(evalArmString("[resourceGroup().location]", CTX)).toBe("eastus");
22
- expect(evalArmString("[resourceGroup().id]", CTX)).toBe("/subscriptions/sub-1/resourceGroups/chant-rg");
35
+ describe("evalArmString — static functions (#707)", () => {
36
+ test("resourceGroup / subscription / concat / uniqueString", async () => {
37
+ expect(await evalArmString("[resourceGroup().location]", ctx())).toBe("eastus");
38
+ expect(await evalArmString("[resourceGroup().id]", ctx())).toBe("/subscriptions/sub-1/resourceGroups/chant-rg");
39
+ expect(await evalArmString("[subscription().subscriptionId]", ctx())).toBe("sub-1");
40
+ const v = await evalArmString("[concat('store', uniqueString(resourceGroup().id))]", ctx());
41
+ expect(String(v)).toHaveLength("store".length + 13);
23
42
  });
24
43
 
25
- test("subscription().subscriptionId", () => {
26
- expect(evalArmString("[subscription().subscriptionId]", CTX)).toBe("sub-1");
44
+ test("resourceId('type','name') → the resource-id path", async () => {
45
+ expect(await evalArmString("[resourceId('Microsoft.Web/serverfarms', 'plan1')]", ctx())).toBe(
46
+ "/subscriptions/sub-1/resourceGroups/chant-rg/providers/Microsoft.Web/serverfarms/plan1",
47
+ );
27
48
  });
28
49
 
29
- test("concat with a literal and a nested function", () => {
30
- const v = evalArmString("[concat('store', uniqueString(resourceGroup().id))]", CTX);
31
- expect(v.startsWith("store")).toBe(true);
32
- expect(v).toHaveLength("store".length + 13); // uniqueString → 13 chars
50
+ test("non-expression + [[ escape passthrough", async () => {
51
+ expect(await evalArmString("plain", ctx())).toBe("plain");
52
+ expect(await evalArmString("[[literal]", ctx())).toBe("[literal]");
33
53
  });
54
+ });
34
55
 
35
- test("uniqueString is deterministic for the same inputs", () => {
36
- const a = evalArmString("[uniqueString(resourceGroup().id)]", CTX);
37
- const b = evalArmString("[uniqueString(resourceGroup().id)]", CTX);
38
- expect(a).toBe(b);
56
+ describe("evalArmString reference() (#707)", () => {
57
+ test("reference('name') → the applied resource's properties, with .prop access", async () => {
58
+ const deployed = new Map<string, unknown>([
59
+ ["mystore", { properties: { primaryEndpoints: { blob: "http://mystore.blob/" } } }],
60
+ ]);
61
+ expect(await evalArmString("[reference('mystore').primaryEndpoints.blob]", ctx({ deployed }))).toBe(
62
+ "http://mystore.blob/",
63
+ );
39
64
  });
65
+ });
40
66
 
41
- test("non-expression strings pass through; [[ is an escaped literal", () => {
42
- expect(evalArmString("plain-name", CTX)).toBe("plain-name");
43
- expect(evalArmString("[[literal]", CTX)).toBe("[literal]");
67
+ describe("evalArmString listKeys() (#707)", () => {
68
+ test("listKeys(resourceId(...), v).keys[0].value → POSTs the key action and indexes", async () => {
69
+ const calls: string[] = [];
70
+ const http: AzHttp = async (method, url) => {
71
+ calls.push(`${method} ${url}`);
72
+ return { status: 200, text: JSON.stringify({ keys: [{ value: "SECRET-KEY" }, { value: "k2" }] }) };
73
+ };
74
+ const expr = "[concat('AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'st'), '2023-01-01').keys[0].value)]";
75
+ expect(await evalArmString(expr, ctx({ http }))).toBe("AccountKey=SECRET-KEY");
76
+ expect(calls[0]).toBe("POST http://x/subscriptions/sub-1/resourceGroups/chant-rg/providers/Microsoft.Storage/storageAccounts/st/listKeys?api-version=2023-01-01");
44
77
  });
78
+ });
45
79
 
46
- test("evalArm recurses into objects and arrays", () => {
47
- expect(evalArm({ a: "[resourceGroup().location]", b: ["[subscription().subscriptionId]", 1] }, CTX)).toEqual({
80
+ describe("evalArm recursion (#707)", () => {
81
+ test("recurses objects/arrays, resolving async expressions", async () => {
82
+ expect(await evalArm({ a: "[resourceGroup().location]", b: ["[subscription().subscriptionId]", 1] }, ctx())).toEqual({
48
83
  a: "eastus",
49
84
  b: ["sub-1", 1],
50
85
  });
51
86
  });
52
87
  });
53
88
 
89
+ describe("dependency ordering (#707)", () => {
90
+ const plan: ArmResource = { type: "Microsoft.Web/serverfarms", apiVersion: "2023-01-01", name: "plan1" };
91
+ const store: ArmResource = { type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "st1" };
92
+ const site: ArmResource = {
93
+ type: "Microsoft.Web/sites",
94
+ apiVersion: "2023-01-01",
95
+ name: "site1",
96
+ properties: {
97
+ serverFarmId: "[resourceId('Microsoft.Web/serverfarms', 'plan1')]",
98
+ conn: "[listKeys(resourceId('Microsoft.Storage/storageAccounts', 'st1'), '2023-01-01')]",
99
+ },
100
+ };
101
+
102
+ test("armDependencies finds referenced resource names in the template", () => {
103
+ expect(armDependencies(site, new Set(["plan1", "st1", "site1"])).sort()).toEqual(["plan1", "st1"]);
104
+ expect(armDependencies(plan, new Set(["plan1", "st1", "site1"]))).toEqual([]);
105
+ });
106
+
107
+ test("orderArmResources applies dependencies before the referrer", () => {
108
+ const ordered = orderArmResources([site, plan, store]).map((r) => r.name);
109
+ expect(ordered.indexOf("plan1")).toBeLessThan(ordered.indexOf("site1"));
110
+ expect(ordered.indexOf("st1")).toBeLessThan(ordered.indexOf("site1"));
111
+ });
112
+
113
+ test("throws on a cycle", () => {
114
+ const a: ArmResource = { type: "T", apiVersion: "v", name: "a", properties: { r: "[resourceId('T', 'b')]" } };
115
+ const b: ArmResource = { type: "T", apiVersion: "v", name: "b", properties: { r: "[resourceId('T', 'a')]" } };
116
+ expect(() => orderArmResources([a, b])).toThrow(/reference cycle/);
117
+ });
118
+ });
119
+
54
120
  const STORAGE: ArmResource = {
55
121
  type: "Microsoft.Storage/storageAccounts",
56
122
  apiVersion: "2025-06-01",
@@ -62,15 +128,15 @@ const STORAGE: ArmResource = {
62
128
  tags: { "managed-by": "chant" },
63
129
  };
64
130
 
65
- describe("armResourceUrl / armResourceBody (#706)", () => {
66
- test("URL is the resource-id PUT path with api-version", () => {
67
- expect(armResourceUrl(STORAGE, CTX, "http://x")).toBe(
131
+ describe("armResourceUrl / armResourceBody (#707)", () => {
132
+ test("URL is the resource-id PUT path", async () => {
133
+ expect(await armResourceUrl(STORAGE, ctx())).toBe(
68
134
  "http://x/subscriptions/sub-1/resourceGroups/chant-rg/providers/Microsoft.Storage/storageAccounts/chantstore1?api-version=2025-06-01",
69
135
  );
70
136
  });
71
137
 
72
- test("body evaluates location, keeps sku/kind/tags/properties", () => {
73
- expect(armResourceBody(STORAGE, CTX)).toEqual({
138
+ test("body evaluates location, keeps sku/kind/tags/properties", async () => {
139
+ expect(await armResourceBody(STORAGE, ctx())).toEqual({
74
140
  location: "eastus",
75
141
  sku: { name: "Standard_LRS" },
76
142
  kind: "StorageV2",
@@ -80,28 +146,33 @@ describe("armResourceUrl / armResourceBody (#706)", () => {
80
146
  });
81
147
  });
82
148
 
83
- describe("azApply flow (#706)", () => {
84
- test("ensures the resource group, then PUTs each resource", async () => {
149
+ describe("azApply flow (#707)", () => {
150
+ test("ensures the resource group, applies in dependency order, captures state", async () => {
85
151
  const calls: Array<{ method: string; url: string }> = [];
86
152
  const http: AzHttp = async (method, url) => {
87
153
  calls.push({ method, url });
88
154
  return { status: 200, text: "{}" };
89
155
  };
90
- // Stub the template read via a data: path — azApply reads a file, so drive it
91
- // through the pure pieces instead by asserting the call sequence a real run makes.
92
- // Here we exercise the HTTP contract with a hand-built template file.
93
156
  const fs = await import("node:fs");
94
157
  const tmp = `/tmp/chant-arm-${process.pid}.json`;
95
- fs.writeFileSync(tmp, JSON.stringify({ resources: [STORAGE] }));
158
+ const site: ArmResource = {
159
+ type: "Microsoft.Web/sites",
160
+ apiVersion: "2023-01-01",
161
+ name: "site1",
162
+ properties: { serverFarmId: "[resourceId('Microsoft.Web/serverfarms', 'plan1')]" },
163
+ };
164
+ const plan: ArmResource = { type: "Microsoft.Web/serverfarms", apiVersion: "2023-01-01", name: "plan1" };
165
+ fs.writeFileSync(tmp, JSON.stringify({ resources: [site, plan] })); // listed out of order
96
166
  const res = await azApply({ templatePath: tmp, resourceGroup: "chant-rg", location: "eastus", endpoint: "http://x", subscriptionId: "sub-1" }, undefined, http);
97
167
  fs.unlinkSync(tmp);
98
- expect(res.applied).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "chantstore1" }]);
99
- expect(calls[0].method).toBe("PUT");
100
- expect(calls[0].url).toContain("/resourceGroups/chant-rg?api-version=");
101
- expect(calls[1].url).toContain("/providers/Microsoft.Storage/storageAccounts/chantstore1?api-version=2025-06-01");
168
+ // plan (dependency) applied before site (referrer), despite manifest order.
169
+ expect(res.applied.map((a) => a.name)).toEqual(["plan1", "site1"]);
170
+ const puts = calls.filter((c) => c.method === "PUT" && c.url.includes("/providers/"));
171
+ expect(puts[0].url).toContain("/serverfarms/plan1");
172
+ expect(puts[1].url).toContain("/sites/site1");
102
173
  });
103
174
 
104
- test("surfaces a resource apply failure (RG ok, resource PUT fails)", async () => {
175
+ test("surfaces a resource apply failure", async () => {
105
176
  const fs = await import("node:fs");
106
177
  const tmp = `/tmp/chant-arm-fail-${process.pid}.json`;
107
178
  fs.writeFileSync(tmp, JSON.stringify({ resources: [STORAGE] }));
@@ -112,4 +183,149 @@ describe("azApply flow (#706)", () => {
112
183
  ).rejects.toThrow(/Microsoft.Storage\/storageAccounts chantstore1 apply failed \(400\)/);
113
184
  fs.unlinkSync(tmp);
114
185
  });
186
+
187
+ test("stamps chant ownership on the PUT body", async () => {
188
+ const fs = await import("node:fs");
189
+ const tmp = `/tmp/chant-arm-own-${process.pid}.json`;
190
+ fs.writeFileSync(tmp, JSON.stringify({ resources: [{ type: "T", apiVersion: "v", name: "r1" }] }));
191
+ let putBody: Record<string, unknown> | undefined;
192
+ const http: AzHttp = async (method, url, body) => {
193
+ if (method === "PUT" && url.includes("/providers/")) putBody = body as Record<string, unknown>;
194
+ return { status: 200, text: "{}" };
195
+ };
196
+ await azApply({ templatePath: tmp, resourceGroup: "rg", endpoint: "http://x" }, undefined, http);
197
+ fs.unlinkSync(tmp);
198
+ expect((putBody?.tags as Record<string, string>)["managed-by"]).toBe("chant");
199
+ });
200
+ });
201
+
202
+ describe("ownership helpers (#azure-prune)", () => {
203
+ test("chantOwnershipTags / isChantOwned", () => {
204
+ expect(chantOwnershipTags()).toEqual({ "managed-by": "chant" });
205
+ expect(isChantOwned({ "managed-by": "chant" })).toBe(true);
206
+ expect(isChantOwned({ "managed-by": "someone-else" })).toBe(false);
207
+ expect(isChantOwned(undefined)).toBe(false);
208
+ });
209
+ });
210
+
211
+ describe("deleteArmResource (#azure-prune)", () => {
212
+ test("DELETEs the resource-id path; 404 is not-deleted", async () => {
213
+ const calls: string[] = [];
214
+ const http: AzHttp = async (method, url) => {
215
+ calls.push(`${method} ${url}`);
216
+ return { status: 200, text: "" };
217
+ };
218
+ const res = await deleteArmResource("Microsoft.Storage/storageAccounts", "st1", "2023-01-01", ctx(), http);
219
+ expect(res).toEqual({ type: "Microsoft.Storage/storageAccounts", name: "st1", deleted: true });
220
+ expect(calls[0]).toBe(
221
+ "DELETE http://x/subscriptions/sub-1/resourceGroups/chant-rg/providers/Microsoft.Storage/storageAccounts/st1?api-version=2023-01-01",
222
+ );
223
+ const gone = await deleteArmResource("T", "x", "v", ctx(), async () => ({ status: 404, text: "" }));
224
+ expect(gone.deleted).toBe(false);
225
+ });
226
+
227
+ test("throws on a non-404 error", async () => {
228
+ await expect(
229
+ deleteArmResource("T", "x", "v", ctx(), async () => ({ status: 403, text: "no" })),
230
+ ).rejects.toThrow(/T x delete failed \(403\)/);
231
+ });
232
+ });
233
+
234
+ describe("listGroupResources (#azure-prune)", () => {
235
+ test("returns the value[] items, filtering malformed entries", async () => {
236
+ const http: AzHttp = async () => ({
237
+ status: 200,
238
+ text: JSON.stringify({ value: [{ id: "/a", name: "a", type: "T", tags: { "managed-by": "chant" } }, { id: "/bad" }] }),
239
+ });
240
+ const items = await listGroupResources(ctx(), http);
241
+ expect(items.map((i) => i.name)).toEqual(["a"]);
242
+ });
243
+
244
+ test("returns [] on an error status", async () => {
245
+ expect(await listGroupResources(ctx(), async () => ({ status: 500, text: "" }))).toEqual([]);
246
+ });
247
+ });
248
+
249
+ describe("pruneArmOrphans (#azure-prune)", () => {
250
+ const desired: ArmResource[] = [{ type: "Microsoft.Storage/storageAccounts", apiVersion: "2023-01-01", name: "keep1" }];
251
+
252
+ test("deletes only chant-owned, templated-type resources not in the template", async () => {
253
+ const live = {
254
+ value: [
255
+ { id: "/1", name: "keep1", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }, // in template → keep
256
+ { id: "/2", name: "orphan1", type: "Microsoft.Storage/storageAccounts", tags: { "managed-by": "chant" } }, // owned, not in template → prune
257
+ { id: "/3", name: "foreign", type: "Microsoft.Storage/storageAccounts", tags: {} }, // not owned → skip
258
+ { id: "/4", name: "othertype", type: "Microsoft.Web/sites", tags: { "managed-by": "chant" } }, // type not templated → skip
259
+ ],
260
+ };
261
+ const deletes: string[] = [];
262
+ const http: AzHttp = async (method, url) => {
263
+ if (method === "DELETE") deletes.push(url);
264
+ return { status: 200, text: method === "GET" ? JSON.stringify(live) : "" };
265
+ };
266
+ const pruned = await pruneArmOrphans(desired, ctx(), http);
267
+ expect(pruned).toEqual([{ type: "Microsoft.Storage/storageAccounts", name: "orphan1", deleted: true }]);
268
+ expect(deletes).toHaveLength(1);
269
+ expect(deletes[0]).toContain("/storageAccounts/orphan1?api-version=2023-01-01"); // apiVersion from the template
270
+ });
271
+ });
272
+
273
+ describe("azApply prune flag (#azure-prune)", () => {
274
+ test("prunes owned orphans of a templated type after applying", async () => {
275
+ const fs = await import("node:fs");
276
+ const tmp = `/tmp/chant-arm-prune-${process.pid}.json`;
277
+ fs.writeFileSync(tmp, JSON.stringify({ resources: [{ type: "T", apiVersion: "v", name: "keep1" }] }));
278
+ const live = { value: [{ id: "/o", name: "orphan1", type: "T", tags: { "managed-by": "chant" } }] };
279
+ const deletes: string[] = [];
280
+ const http: AzHttp = async (method, url) => {
281
+ if (method === "DELETE") deletes.push(url);
282
+ return { status: 200, text: method === "GET" ? JSON.stringify(live) : "{}" };
283
+ };
284
+ const res = await azApply({ templatePath: tmp, resourceGroup: "rg", endpoint: "http://x", prune: true }, undefined, http);
285
+ fs.unlinkSync(tmp);
286
+ expect(res.applied.map((a) => a.name)).toEqual(["keep1"]);
287
+ expect(res.pruned).toEqual([{ type: "T", name: "orphan1", deleted: true }]);
288
+ expect(deletes[0]).toContain("/providers/T/orphan1");
289
+ });
290
+
291
+ test("no prune when the flag is off", async () => {
292
+ const fs = await import("node:fs");
293
+ const tmp = `/tmp/chant-arm-noprune-${process.pid}.json`;
294
+ fs.writeFileSync(tmp, JSON.stringify({ resources: [{ type: "T", apiVersion: "v", name: "keep1" }] }));
295
+ let listed = false;
296
+ const http: AzHttp = async (method, url) => {
297
+ if (method === "GET" && url.includes("/resources?")) listed = true;
298
+ return { status: 200, text: "{}" };
299
+ };
300
+ const res = await azApply({ templatePath: tmp, resourceGroup: "rg", endpoint: "http://x" }, undefined, http);
301
+ fs.unlinkSync(tmp);
302
+ expect(res.pruned).toEqual([]);
303
+ expect(listed).toBe(false);
304
+ });
305
+ });
306
+
307
+ describe("azDelete (#azure-prune)", () => {
308
+ test("deletes declared resources in reverse dependency order", async () => {
309
+ const fs = await import("node:fs");
310
+ const tmp = `/tmp/chant-arm-del-${process.pid}.json`;
311
+ const site: ArmResource = {
312
+ type: "Microsoft.Web/sites",
313
+ apiVersion: "2023-01-01",
314
+ name: "site1",
315
+ properties: { serverFarmId: "[resourceId('Microsoft.Web/serverfarms', 'plan1')]" },
316
+ };
317
+ const plan: ArmResource = { type: "Microsoft.Web/serverfarms", apiVersion: "2023-01-01", name: "plan1" };
318
+ fs.writeFileSync(tmp, JSON.stringify({ resources: [plan, site] }));
319
+ const deletes: string[] = [];
320
+ const http: AzHttp = async (method, url) => {
321
+ if (method === "DELETE") deletes.push(url);
322
+ return { status: 200, text: "" };
323
+ };
324
+ const res = await azDelete({ templatePath: tmp, resourceGroup: "chant-rg", endpoint: "http://x" }, undefined, http);
325
+ fs.unlinkSync(tmp);
326
+ // referrer (site) deleted before the resource it references (plan).
327
+ expect(res.deleted.map((d) => d.name)).toEqual(["site1", "plan1"]);
328
+ expect(deletes[0]).toContain("/sites/site1");
329
+ expect(deletes[1]).toContain("/serverfarms/plan1");
330
+ });
115
331
  });