@intentius/chant 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/generate.d.ts +16 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +257 -0
- package/dist/deep-observation.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +23 -3
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +9 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +44 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +18 -1
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon.d.ts +47 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +103 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -0
- package/dist/lifecycle/deep-observe.d.ts +62 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +118 -0
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/graph.test.ts +86 -0
- package/src/cli/handlers/graph.ts +64 -3
- package/src/cli/handlers/lifecycle.test.ts +126 -1
- package/src/cli/handlers/lifecycle.ts +184 -3
- package/src/cli/main.test.ts +6 -0
- package/src/cli/main.ts +12 -0
- package/src/cli/registry.ts +14 -0
- package/src/codegen/generate.ts +25 -0
- package/src/deep-observation.test.ts +234 -0
- package/src/deep-observation.ts +489 -0
- package/src/discovery/fold-import.test.ts +372 -1
- package/src/discovery/fold-import.ts +235 -79
- package/src/fold/fold.test.ts +105 -0
- package/src/fold/fold.ts +88 -18
- package/src/fold/subset.test.ts +38 -7
- package/src/fold/subset.ts +9 -0
- package/src/graph-ir.ts +47 -0
- package/src/index.ts +1 -0
- package/src/kubectl-context.ts +22 -2
- package/src/lexicon.ts +59 -0
- package/src/lifecycle/deep-diff.test.ts +157 -0
- package/src/lifecycle/deep-diff.ts +213 -0
- package/src/lifecycle/deep-observe.test.ts +174 -0
- package/src/lifecycle/deep-observe.ts +173 -0
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +99 -0
- package/src/lifecycle/observation-baseline.ts +217 -0
- package/src/lifecycle/snapshot.ts +6 -11
|
@@ -3,7 +3,7 @@ import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { join, dirname, resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { tryFoldFile, createFoldSession } from "./fold-import";
|
|
6
|
+
import { tryFoldFile, createFoldSession, planFoldTaint } from "./fold-import";
|
|
7
7
|
import { isDeclarable } from "../declarable";
|
|
8
8
|
import { isCompositeInstance } from "../composite";
|
|
9
9
|
import { isAttrRefLike } from "../utils";
|
|
@@ -1923,3 +1923,374 @@ describe("tryFoldFile — active lexicon package exports (#1063)", () => {
|
|
|
1923
1923
|
expect(result.reason).toContain(`ambient "process" read is not foldable`);
|
|
1924
1924
|
});
|
|
1925
1925
|
});
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* chant #1169 — a `new Type(...)` used as a VALUE.
|
|
1929
|
+
*
|
|
1930
|
+
* The largest measured gate on the example corpus (64 files, the sole blocker
|
|
1931
|
+
* in 22 entries) and the one the original #1022-era rejection was most
|
|
1932
|
+
* deliberate about: `fold()` alone can only produce a `{__resource, props}`
|
|
1933
|
+
* envelope, and an envelope that reaches a serializer is not the value the run
|
|
1934
|
+
* path produced — `image: new Image({ name })` has to serialize as the
|
|
1935
|
+
* constructed Image's own shape, not as the envelope.
|
|
1936
|
+
*
|
|
1937
|
+
* The answer is not to normalize the envelope; it is to construct. These tests
|
|
1938
|
+
* are about proving that what gets constructed is INDISTINGUISHABLE from what
|
|
1939
|
+
* running the file would have built:
|
|
1940
|
+
*
|
|
1941
|
+
* - a real instance of the class the file's own `import` names, with its
|
|
1942
|
+
* prototype and `kind`/`entityType` intact (not a plain-object copy);
|
|
1943
|
+
* - built ONCE per source construction, so a resource named by a `const` and
|
|
1944
|
+
* referenced three times is one object — the same object discovery
|
|
1945
|
+
* registers, which is the only reason a reference to it can resolve to a
|
|
1946
|
+
* logical name at all;
|
|
1947
|
+
* - behind the same #1093 gate as every other construction, so `--sandbox`
|
|
1948
|
+
* still executes nothing project-owned in this process.
|
|
1949
|
+
*/
|
|
1950
|
+
describe("tryFoldFile — constructions as values (chant #1169)", () => {
|
|
1951
|
+
let testDir: string;
|
|
1952
|
+
|
|
1953
|
+
beforeEach(async () => {
|
|
1954
|
+
testDir = join(tmpdir(), `chant-fold-import-1169-test-${Date.now()}-${Math.random()}`);
|
|
1955
|
+
await mkdir(testDir, { recursive: true });
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
afterEach(async () => {
|
|
1959
|
+
await rm(testDir, { recursive: true, force: true });
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
/** A resource class and two property-kind classes, from chant's real runtime factories — the exact shapes every lexicon's generated barrel produces. */
|
|
1963
|
+
async function writeDefs(): Promise<void> {
|
|
1964
|
+
await writeFile(
|
|
1965
|
+
join(testDir, "resources.ts"),
|
|
1966
|
+
`
|
|
1967
|
+
import { createResource, createProperty } from ${JSON.stringify(runtimePath)};
|
|
1968
|
+
export const Job = createResource("Test::Job", "gitlab", { Id: "Id" });
|
|
1969
|
+
export const Image = createProperty("Test::Image", "gitlab");
|
|
1970
|
+
export const Rule = createProperty("Test::Rule", "gitlab");
|
|
1971
|
+
`,
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/** The `props` of a folded entity, without the `as unknown as` dance at every call site. */
|
|
1976
|
+
function propsOf(entity: unknown): Record<string, unknown> {
|
|
1977
|
+
return (entity as { props: Record<string, unknown> }).props;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
test("an INLINE nested construction becomes a real instance, not a `{__resource}` envelope", async () => {
|
|
1981
|
+
await writeDefs();
|
|
1982
|
+
const file = join(testDir, "main.ts");
|
|
1983
|
+
await writeFile(
|
|
1984
|
+
file,
|
|
1985
|
+
`
|
|
1986
|
+
import { Job, Image } from "./resources";
|
|
1987
|
+
throw new Error("must never execute — sentinel for #1169 fold verification");
|
|
1988
|
+
export const build = new Job({ stage: "build", image: new Image({ name: "node:22-alpine" }) });
|
|
1989
|
+
`,
|
|
1990
|
+
);
|
|
1991
|
+
|
|
1992
|
+
const result = await tryFoldFile(file);
|
|
1993
|
+
|
|
1994
|
+
expect(result.ok).toBe(true);
|
|
1995
|
+
if (!result.ok) return;
|
|
1996
|
+
const [, entity] = result.entities[0];
|
|
1997
|
+
const image = propsOf(entity).image;
|
|
1998
|
+
// The whole point: a live Declarable with its prototype, `kind` and
|
|
1999
|
+
// `entityType`, which is what the serializer's walker dispatches on. An
|
|
2000
|
+
// envelope would be a plain object with a `__resource` key and would
|
|
2001
|
+
// serialize as `{ __resource, props }`.
|
|
2002
|
+
expect(isDeclarable(image)).toBe(true);
|
|
2003
|
+
expect((image as unknown as { entityType: string }).entityType).toBe("Test::Image");
|
|
2004
|
+
expect((image as unknown as { kind: string }).kind).toBe("property");
|
|
2005
|
+
expect(propsOf(image)).toEqual({ name: "node:22-alpine" });
|
|
2006
|
+
expect(image).not.toHaveProperty("__resource");
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
test("constructions nest in every value position — an array element, a deeper object, a second level", async () => {
|
|
2010
|
+
await writeDefs();
|
|
2011
|
+
const file = join(testDir, "main.ts");
|
|
2012
|
+
await writeFile(
|
|
2013
|
+
file,
|
|
2014
|
+
`
|
|
2015
|
+
import { Job, Image, Rule } from "./resources";
|
|
2016
|
+
export const deploy = new Job({
|
|
2017
|
+
image: new Image({ name: "alpine" }),
|
|
2018
|
+
rules: [new Rule({ if: "$CI_COMMIT_BRANCH" }), new Rule({ when: "manual" })],
|
|
2019
|
+
nested: { inner: new Rule({ if: "always" }) },
|
|
2020
|
+
});
|
|
2021
|
+
`,
|
|
2022
|
+
);
|
|
2023
|
+
|
|
2024
|
+
const result = await tryFoldFile(file);
|
|
2025
|
+
|
|
2026
|
+
expect(result.ok).toBe(true);
|
|
2027
|
+
if (!result.ok) return;
|
|
2028
|
+
const props = propsOf(result.entities[0][1]);
|
|
2029
|
+
const rules = props.rules as unknown[];
|
|
2030
|
+
expect(rules).toHaveLength(2);
|
|
2031
|
+
expect(rules.every((r) => isDeclarable(r))).toBe(true);
|
|
2032
|
+
expect(propsOf(rules[0])).toEqual({ if: "$CI_COMMIT_BRANCH" });
|
|
2033
|
+
expect(isDeclarable((props.nested as { inner: unknown }).inner)).toBe(true);
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
test("the constructor's second argument (CFN-style attributes) folds a nested construction too", async () => {
|
|
2037
|
+
await writeDefs();
|
|
2038
|
+
const file = join(testDir, "main.ts");
|
|
2039
|
+
await writeFile(
|
|
2040
|
+
file,
|
|
2041
|
+
`
|
|
2042
|
+
import { Job, Rule } from "./resources";
|
|
2043
|
+
export const job = new Job({ stage: "test" }, { Metadata: new Rule({ note: "attr" }) });
|
|
2044
|
+
`,
|
|
2045
|
+
);
|
|
2046
|
+
|
|
2047
|
+
const result = await tryFoldFile(file);
|
|
2048
|
+
|
|
2049
|
+
expect(result.ok).toBe(true);
|
|
2050
|
+
if (!result.ok) return;
|
|
2051
|
+
const attributes = (result.entities[0][1] as unknown as { attributes: Record<string, unknown> }).attributes;
|
|
2052
|
+
expect(isDeclarable(attributes.Metadata)).toBe(true);
|
|
2053
|
+
expect(propsOf(attributes.Metadata)).toEqual({ note: "attr" });
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
test("a NAMED same-file resource is built ONCE — every reference is the same object", async () => {
|
|
2057
|
+
await writeDefs();
|
|
2058
|
+
const file = join(testDir, "main.ts");
|
|
2059
|
+
await writeFile(
|
|
2060
|
+
file,
|
|
2061
|
+
`
|
|
2062
|
+
import { Job, Image } from "./resources";
|
|
2063
|
+
const nodeImage = new Image({ name: "node:22-alpine" });
|
|
2064
|
+
export const build = new Job({ stage: "build", image: nodeImage });
|
|
2065
|
+
export const check = new Job({ stage: "test", image: nodeImage });
|
|
2066
|
+
`,
|
|
2067
|
+
);
|
|
2068
|
+
|
|
2069
|
+
const result = await tryFoldFile(file);
|
|
2070
|
+
|
|
2071
|
+
expect(result.ok).toBe(true);
|
|
2072
|
+
if (!result.ok) return;
|
|
2073
|
+
const buildImage = propsOf(result.exportedValues.get("build")).image;
|
|
2074
|
+
const checkImage = propsOf(result.exportedValues.get("check")).image;
|
|
2075
|
+
expect(isDeclarable(buildImage)).toBe(true);
|
|
2076
|
+
// Identity, not equality. Running the module evaluates `const nodeImage`
|
|
2077
|
+
// once and both jobs close over that one object; two instances here would
|
|
2078
|
+
// be a different program.
|
|
2079
|
+
expect(buildImage).toBe(checkImage);
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
test("a named resource referenced by another resource IS the entity discovery registers — no duplicate", async () => {
|
|
2083
|
+
await writeDefs();
|
|
2084
|
+
const file = join(testDir, "main.ts");
|
|
2085
|
+
await writeFile(
|
|
2086
|
+
file,
|
|
2087
|
+
`
|
|
2088
|
+
import { Job } from "./resources";
|
|
2089
|
+
export const first = new Job({ stage: "first" });
|
|
2090
|
+
export const second = new Job({ stage: "second", needs: [first] });
|
|
2091
|
+
`,
|
|
2092
|
+
);
|
|
2093
|
+
|
|
2094
|
+
const result = await tryFoldFile(file);
|
|
2095
|
+
|
|
2096
|
+
expect(result.ok).toBe(true);
|
|
2097
|
+
if (!result.ok) return;
|
|
2098
|
+
const first = result.exportedValues.get("first");
|
|
2099
|
+
const needs = propsOf(result.exportedValues.get("second")).needs as unknown[];
|
|
2100
|
+
// THE assertion this whole design turns on. `entities` is what discovery
|
|
2101
|
+
// assigns logical names to; if the reference held a second instance, the
|
|
2102
|
+
// serializer would find it absent from that table and inline its props
|
|
2103
|
+
// where the run path emits a reference — or throw "logical name not set".
|
|
2104
|
+
expect(needs[0]).toBe(first);
|
|
2105
|
+
expect(result.entities.map(([name]) => name)).toEqual(["first", "second"]);
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
test("`export { x }` of a same-file resource exports that same instance", async () => {
|
|
2109
|
+
await writeDefs();
|
|
2110
|
+
const file = join(testDir, "main.ts");
|
|
2111
|
+
await writeFile(
|
|
2112
|
+
file,
|
|
2113
|
+
`
|
|
2114
|
+
import { Job, Image } from "./resources";
|
|
2115
|
+
const shared = new Image({ name: "alpine" });
|
|
2116
|
+
const job = new Job({ image: shared });
|
|
2117
|
+
export { job };
|
|
2118
|
+
`,
|
|
2119
|
+
);
|
|
2120
|
+
|
|
2121
|
+
const result = await tryFoldFile(file);
|
|
2122
|
+
|
|
2123
|
+
expect(result.ok).toBe(true);
|
|
2124
|
+
if (!result.ok) return;
|
|
2125
|
+
expect(result.entities.map(([name]) => name)).toEqual(["job"]);
|
|
2126
|
+
expect(isDeclarable(propsOf(result.exportedValues.get("job")).image)).toBe(true);
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
test("an attribute reference to a same-file resource still folds to the `{__attrRef}` envelope, unchanged", async () => {
|
|
2130
|
+
await writeDefs();
|
|
2131
|
+
const file = join(testDir, "main.ts");
|
|
2132
|
+
await writeFile(
|
|
2133
|
+
file,
|
|
2134
|
+
`
|
|
2135
|
+
import { Job } from "./resources";
|
|
2136
|
+
export const first = new Job({ stage: "first" });
|
|
2137
|
+
export const second = new Job({ upstream: first.Id });
|
|
2138
|
+
`,
|
|
2139
|
+
);
|
|
2140
|
+
|
|
2141
|
+
const result = await tryFoldFile(file);
|
|
2142
|
+
|
|
2143
|
+
expect(result.ok).toBe(true);
|
|
2144
|
+
if (!result.ok) return;
|
|
2145
|
+
// #1169 deliberately leaves this path alone: a sibling ATTRIBUTE reference
|
|
2146
|
+
// is resolved by the serializer's own walker from the envelope's entity
|
|
2147
|
+
// NAME, and nothing about that had to change for a construction to fold.
|
|
2148
|
+
expect(propsOf(result.exportedValues.get("second")).upstream).toEqual({
|
|
2149
|
+
__attrRef: { entity: "first", attribute: "Id" },
|
|
2150
|
+
});
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
test("a nested construction whose class isn't a resolvable import falls the file back to run", async () => {
|
|
2154
|
+
await writeDefs();
|
|
2155
|
+
const file = join(testDir, "main.ts");
|
|
2156
|
+
await writeFile(
|
|
2157
|
+
file,
|
|
2158
|
+
`
|
|
2159
|
+
import { Job } from "./resources";
|
|
2160
|
+
export const job = new Job({ image: new Unimported({ name: "x" }) });
|
|
2161
|
+
`,
|
|
2162
|
+
);
|
|
2163
|
+
|
|
2164
|
+
const result = await tryFoldFile(file);
|
|
2165
|
+
|
|
2166
|
+
expect(result.ok).toBe(false);
|
|
2167
|
+
if (result.ok) return;
|
|
2168
|
+
expect(result.reason).toContain(`constructor "Unimported" is not a resolvable import`);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test("a nested `new ns.Type(...)` through a namespace import stays rejected", async () => {
|
|
2172
|
+
await writeDefs();
|
|
2173
|
+
const file = join(testDir, "main.ts");
|
|
2174
|
+
await writeFile(
|
|
2175
|
+
file,
|
|
2176
|
+
`
|
|
2177
|
+
import { Job } from "./resources";
|
|
2178
|
+
import * as res from "./resources";
|
|
2179
|
+
export const job = new Job({ image: new res.Image({ name: "x" }) });
|
|
2180
|
+
`,
|
|
2181
|
+
);
|
|
2182
|
+
|
|
2183
|
+
const result = await tryFoldFile(file);
|
|
2184
|
+
|
|
2185
|
+
expect(result.ok).toBe(false);
|
|
2186
|
+
if (result.ok) return;
|
|
2187
|
+
// Shape-level: a dotted callee cannot be resolved to a live class through
|
|
2188
|
+
// the file's named imports, so it never folds to an envelope nothing could
|
|
2189
|
+
// revive. Same bare-identifier rule as an intrinsic call and #1023's
|
|
2190
|
+
// factory-body construction.
|
|
2191
|
+
expect(result.reason).toContain("needs a plain imported constructor");
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
test("a same-file resource used as a value rejects when no construction is available", async () => {
|
|
2195
|
+
// The divergence #1169 adds, in the one direction that is safe. When the
|
|
2196
|
+
// class can't be resolved there is no instance to point at, and re-folding
|
|
2197
|
+
// the initializer would build a duplicate — so the reference rejects and
|
|
2198
|
+
// the file falls back to run, where both references are the same object by
|
|
2199
|
+
// construction.
|
|
2200
|
+
const file = join(testDir, "main.ts");
|
|
2201
|
+
await writeFile(
|
|
2202
|
+
file,
|
|
2203
|
+
`
|
|
2204
|
+
import { Job } from "./missing-module";
|
|
2205
|
+
const shared = new Job({ stage: "a" });
|
|
2206
|
+
export const job = new Job({ needs: [shared] });
|
|
2207
|
+
`,
|
|
2208
|
+
);
|
|
2209
|
+
|
|
2210
|
+
const result = await tryFoldFile(file);
|
|
2211
|
+
|
|
2212
|
+
expect(result.ok).toBe(false);
|
|
2213
|
+
if (result.ok) return;
|
|
2214
|
+
expect(result.reason).toContain("same-file resource `shared` used as a value is not foldable");
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
test("a construction whose class is a PROJECT file folds under plain --fold and REFUSES under --sandbox", async () => {
|
|
2218
|
+
// chant #1093 — a nested construction reaches its class through exactly the
|
|
2219
|
+
// same binding gate every other construction does, so it cannot widen the
|
|
2220
|
+
// boundary. `./resources.ts` is project source: trusted enough to import
|
|
2221
|
+
// under plain `--fold` (the run path imports it too), never under
|
|
2222
|
+
// `--sandbox`, where the file demotes to the child instead.
|
|
2223
|
+
await writeDefs();
|
|
2224
|
+
const file = join(testDir, "main.ts");
|
|
2225
|
+
await writeFile(
|
|
2226
|
+
file,
|
|
2227
|
+
`
|
|
2228
|
+
import { Job, Image } from "./resources";
|
|
2229
|
+
export const job = new Job({ image: new Image({ name: "alpine" }) });
|
|
2230
|
+
`,
|
|
2231
|
+
);
|
|
2232
|
+
|
|
2233
|
+
const plain = await tryFoldFile(file, [], createFoldSession([], undefined, [], false));
|
|
2234
|
+
expect(plain.ok).toBe(true);
|
|
2235
|
+
|
|
2236
|
+
const sandboxed = await tryFoldFile(file, [], createFoldSession([], undefined, [], true));
|
|
2237
|
+
expect(sandboxed.ok).toBe(false);
|
|
2238
|
+
if (sandboxed.ok) return;
|
|
2239
|
+
expect(sandboxed.reason).toContain("--sandbox");
|
|
2240
|
+
expect(sandboxed.reason).toContain(`constructor "Image"`);
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
test("a nested instance reaches another file only through an exported object, and that edge is already recorded", async () => {
|
|
2244
|
+
// chant #1097's question, answered for #1169. A nested instance is an
|
|
2245
|
+
// inline value: it is never an export of its own, so no other file can name
|
|
2246
|
+
// it. The one way another file CAN observe it is by importing the object
|
|
2247
|
+
// that holds it — and that is an ordinary cross-file object edge, already
|
|
2248
|
+
// recorded in `liveSources` and already reverse-taint-propagated by
|
|
2249
|
+
// `planFoldTaint` (#1044). Nothing new to track.
|
|
2250
|
+
await writeDefs();
|
|
2251
|
+
await writeFile(
|
|
2252
|
+
join(testDir, "shared.ts"),
|
|
2253
|
+
`
|
|
2254
|
+
import { Image } from "./resources";
|
|
2255
|
+
export const cfg = { image: new Image({ name: "alpine" }) };
|
|
2256
|
+
`,
|
|
2257
|
+
);
|
|
2258
|
+
const file = join(testDir, "main.ts");
|
|
2259
|
+
await writeFile(
|
|
2260
|
+
file,
|
|
2261
|
+
`
|
|
2262
|
+
import { Job } from "./resources";
|
|
2263
|
+
import { cfg } from "./shared";
|
|
2264
|
+
export const job = new Job({ image: cfg.image });
|
|
2265
|
+
`,
|
|
2266
|
+
);
|
|
2267
|
+
|
|
2268
|
+
const session = createFoldSession();
|
|
2269
|
+
const sharedPath = join(testDir, "shared.ts");
|
|
2270
|
+
const sharedResult = await tryFoldFile(sharedPath, [], session);
|
|
2271
|
+
const result = await tryFoldFile(file, [], session);
|
|
2272
|
+
|
|
2273
|
+
expect(result.ok).toBe(true);
|
|
2274
|
+
expect(sharedResult.ok).toBe(true);
|
|
2275
|
+
if (!result.ok || !sharedResult.ok) return;
|
|
2276
|
+
|
|
2277
|
+
const sharedImage = (sharedResult.exportedValues.get("cfg") as { image: unknown }).image;
|
|
2278
|
+
// Identity across the file boundary — one instance, shared, exactly as
|
|
2279
|
+
// Node's module cache would give the run path.
|
|
2280
|
+
expect(propsOf(result.exportedValues.get("job")).image).toBe(sharedImage);
|
|
2281
|
+
// And the edge that makes it safe: main.ts is on record as having consumed
|
|
2282
|
+
// shared.ts's objects, so if shared.ts is ever forced back to run, main.ts
|
|
2283
|
+
// is forced with it rather than left holding a stale instance.
|
|
2284
|
+
expect([...result.liveSources]).toContain(sharedPath);
|
|
2285
|
+
|
|
2286
|
+
const tainted = await planFoldTaint(
|
|
2287
|
+
[file, sharedPath],
|
|
2288
|
+
new Map([
|
|
2289
|
+
[file, false],
|
|
2290
|
+
[sharedPath, true],
|
|
2291
|
+
]),
|
|
2292
|
+
new Map([[file, result.liveSources]]),
|
|
2293
|
+
);
|
|
2294
|
+
expect(tainted.has(sharedPath)).toBe(true);
|
|
2295
|
+
});
|
|
2296
|
+
});
|