@intentius/chant 0.93.0 → 0.95.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/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +14 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/operator.d.ts +90 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts +84 -0
- package/dist/op/steward-beside.d.ts.map +1 -0
- package/dist/op/steward.d.ts +87 -2
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/box-intent.d.ts +85 -0
- package/dist/workspace/box-intent.d.ts.map +1 -0
- package/dist/workspace/box-services.d.ts +31 -0
- package/dist/workspace/box-services.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +5 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts +14 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +11 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +5 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +43 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +62 -1
- package/dist/workspace/graph-cache.d.ts +168 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -5
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +5 -5
- package/dist/workspace/kind-readers.d.ts +39 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +29 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +15 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-run.d.ts +2 -0
- package/dist/workspace/member-run.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +3 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +5 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +10 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +4 -2
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +8 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +31 -9
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +20 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +26 -0
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +7 -3
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +97 -0
- package/src/cli/handlers/operator.ts +53 -11
- package/src/cli/handlers/run.test.ts +71 -0
- package/src/cli/handlers/run.ts +65 -7
- package/src/cli/main.ts +21 -9
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/registry.ts +2 -0
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- package/src/cli/static-config-read.test.ts +8 -2
- package/src/meta/source-is-text.test.ts +21 -3
- package/src/okf.test.ts +6 -1
- package/src/op/activities/decide.test.ts +8 -0
- package/src/op/builders.ts +14 -3
- package/src/op/index.ts +8 -2
- package/src/op/operator.ts +264 -16
- package/src/op/steward-beside.test.ts +267 -0
- package/src/op/steward-beside.ts +219 -0
- package/src/op/steward-points.test.ts +112 -1
- package/src/op/steward.ts +135 -3
- package/src/workspace/box-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -0
- package/src/workspace/box-services.test.ts +129 -0
- package/src/workspace/box-services.ts +51 -0
- package/src/workspace/chant-migrations.ts +5 -0
- package/src/workspace/check.schema.json +7 -3
- package/src/workspace/checks/boxes.test.ts +3 -1
- package/src/workspace/checks/boxes.ts +66 -0
- package/src/workspace/checks.ts +12 -1
- package/src/workspace/compose-graph.test.ts +1 -0
- package/src/workspace/compose-graph.ts +11 -0
- package/src/workspace/composites.test.ts +1 -1
- package/src/workspace/composites.ts +12 -5
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -2
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +62 -1
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +26 -0
- package/src/workspace/graph-cache.test.ts +343 -0
- package/src/workspace/graph-cache.ts +409 -0
- package/src/workspace/graph-cli.ts +93 -26
- package/src/workspace/graph-contract.test.ts +129 -6
- package/src/workspace/graph.schema.json +21 -1
- package/src/workspace/intent-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/kind-readers.e2e.test.ts +68 -0
- package/src/workspace/kind-readers.test.ts +134 -0
- package/src/workspace/kind-readers.ts +111 -0
- package/src/workspace/kinds.test.ts +49 -0
- package/src/workspace/kinds.ts +59 -2
- package/src/workspace/member-commands.test.ts +15 -0
- package/src/workspace/member-commands.ts +47 -7
- package/src/workspace/member-run.ts +10 -2
- package/src/workspace/points-cli.ts +3 -0
- package/src/workspace/points.schema.json +18 -0
- package/src/workspace/points.test.ts +15 -14
- package/src/workspace/points.ts +4 -16
- package/src/workspace/read-contract.test.ts +8 -0
- package/src/workspace/reason-codes.test.ts +7 -7
- package/src/workspace/reason-codes.ts +6 -1
- package/src/workspace/records-amend.schema.json +1 -0
- package/src/workspace/records-cli.ts +34 -13
- package/src/workspace/records-contract.test.ts +2 -1
- package/src/workspace/records-formats.test.ts +15 -15
- package/src/workspace/records-new.schema.json +1 -0
- package/src/workspace/records-quorum.test.ts +10 -3
- package/src/workspace/records-sessions-write.test.ts +2 -1
- package/src/workspace/records-since.test.ts +8 -7
- package/src/workspace/records-write.test.ts +101 -1
- package/src/workspace/records-write.ts +50 -6
- package/src/workspace/records.test.ts +1 -1
- package/src/workspace/records.ts +22 -5
- package/src/workspace/status-contract.test.ts +32 -1
- package/src/workspace/status-stewards.ts +88 -9
- package/src/workspace/status.schema.json +66 -4
- package/src/workspace/status.ts +32 -0
- package/src/workspace/trust/record-seal.test.ts +26 -2
- package/src/workspace/work-evidence.schema.json +1 -0
- package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +17 -0
- package/src/workspace/workspace-kinds.schema.json +26 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-member cache behind `chant workspace graph` (#2876, ws-059): a
|
|
3
|
+
* member whose source, install, toolchain, command line and environment are
|
|
4
|
+
* unchanged is answered without starting its chant, and anything else reads.
|
|
5
|
+
*
|
|
6
|
+
* Members run under a fake chant that logs each `graph` it answers, so a test
|
|
7
|
+
* can count the processes a read started. Files are aged past the freshness
|
|
8
|
+
* window before a read that should be stored, since a read of a file younger
|
|
9
|
+
* than the window is never cached.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
15
|
+
import { cleanScratch, commitAll, contract, declaration, repo } from "./__fixtures__/contract-repo";
|
|
16
|
+
import { planMembers } from "./member-commands";
|
|
17
|
+
import {
|
|
18
|
+
environmentStamp,
|
|
19
|
+
graphCacheDir,
|
|
20
|
+
isCacheableArgv,
|
|
21
|
+
memberStamp,
|
|
22
|
+
openGraphCache,
|
|
23
|
+
splitCached,
|
|
24
|
+
VOLATILE_ENV,
|
|
25
|
+
} from "./graph-cache";
|
|
26
|
+
import { workspaceGraph, type GraphDocument } from "./graph-cli";
|
|
27
|
+
import schema from "./graph.schema.json";
|
|
28
|
+
import type { ParsedArgs } from "../cli/registry";
|
|
29
|
+
|
|
30
|
+
afterAll(cleanScratch);
|
|
31
|
+
|
|
32
|
+
const { expectValid } = contract(schema);
|
|
33
|
+
|
|
34
|
+
function result(doc: GraphDocument): Extract<GraphDocument, { nodes: unknown }> {
|
|
35
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
36
|
+
return doc;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A chant older than member-run that logs every `graph` it answers to
|
|
41
|
+
* `<root>/spawns.log`, and touches `race.txt` mid-read when `race` exists.
|
|
42
|
+
*/
|
|
43
|
+
function loggingChant(root: string): string {
|
|
44
|
+
return `#!/bin/sh
|
|
45
|
+
case "$1" in
|
|
46
|
+
graph)
|
|
47
|
+
echo "$PWD" >> "${root}/spawns.log"
|
|
48
|
+
[ -f race ] && echo moved > race.txt
|
|
49
|
+
[ -f fail ] && { echo "Error: broken" >&2; exit 1; }
|
|
50
|
+
printf '{"version":1,"nodes":['
|
|
51
|
+
sep=""
|
|
52
|
+
while read -r id; do printf '%s{"id":"%s","kind":"Thing","lexicon":"fake","attrs":{}}' "$sep" "$id"; sep=","; done < ids.txt
|
|
53
|
+
printf '],"edges":[],"groups":{}}\\n'
|
|
54
|
+
;;
|
|
55
|
+
*) echo "Error: Unknown command: $1" >&2; exit 1 ;;
|
|
56
|
+
esac
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function workspace(): string {
|
|
61
|
+
const root = repo({
|
|
62
|
+
"chant.workspace.json": declaration([
|
|
63
|
+
{ name: "api", dir: "services/api", kind: "chant" },
|
|
64
|
+
{ name: "web", dir: "apps/web", kind: "chant" },
|
|
65
|
+
]),
|
|
66
|
+
"services/api/chant.config.ts": "export default {};\n",
|
|
67
|
+
"services/api/ids.txt": "Queue\n",
|
|
68
|
+
"apps/web/chant.config.ts": "export default {};\n",
|
|
69
|
+
"apps/web/ids.txt": "Site\n",
|
|
70
|
+
".gitignore": "node_modules\nspawns.log\n",
|
|
71
|
+
});
|
|
72
|
+
repoWrite(root, "node_modules/.bin/chant", loggingChant(root), 0o755);
|
|
73
|
+
age(root);
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function repoWrite(root: string, path: string, text: string, mode?: number): void {
|
|
78
|
+
const full = join(root, path);
|
|
79
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
80
|
+
writeFileSync(full, text);
|
|
81
|
+
if (mode !== undefined) chmodSync(full, mode);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Set every file's mtime to one fixed moment long past the freshness window. */
|
|
85
|
+
const THEN = new Date("2026-01-01T00:00:00Z");
|
|
86
|
+
function age(dir: string): void {
|
|
87
|
+
const then = THEN;
|
|
88
|
+
const walk = (at: string): void => {
|
|
89
|
+
for (const e of readdirSync(at, { withFileTypes: true })) {
|
|
90
|
+
if (e.name === ".git") continue;
|
|
91
|
+
const path = join(at, e.name);
|
|
92
|
+
if (e.isDirectory()) walk(path);
|
|
93
|
+
else if (e.isFile()) utimesSync(path, then, then);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(dir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function spawns(root: string): string[] {
|
|
100
|
+
const log = join(root, "spawns.log");
|
|
101
|
+
return existsSync(log) ? readFileSync(log, "utf-8").trim().split("\n").filter(Boolean) : [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("the per-member graph cache (#2876)", () => {
|
|
105
|
+
test("an unchanged member is served without starting its chant", async () => {
|
|
106
|
+
const root = workspace();
|
|
107
|
+
const first = result((await workspaceGraph({ cwd: root })).doc);
|
|
108
|
+
expectValid(first);
|
|
109
|
+
expect(first.members.map((m) => [m.name, m.cached])).toEqual([
|
|
110
|
+
["api", false],
|
|
111
|
+
["web", false],
|
|
112
|
+
]);
|
|
113
|
+
expect(spawns(root)).toHaveLength(2);
|
|
114
|
+
expect(readdirSync(graphCacheDir(root))).toHaveLength(2);
|
|
115
|
+
// A read never changes the workspace it reads: the cache lives elsewhere.
|
|
116
|
+
expect(existsSync(join(root, ".chant"))).toBe(false);
|
|
117
|
+
|
|
118
|
+
const second = result((await workspaceGraph({ cwd: root })).doc);
|
|
119
|
+
expectValid(second);
|
|
120
|
+
expect(spawns(root)).toHaveLength(2);
|
|
121
|
+
expect(second.members.map((m) => [m.name, m.cached])).toEqual([
|
|
122
|
+
["api", true],
|
|
123
|
+
["web", true],
|
|
124
|
+
]);
|
|
125
|
+
expect(second.nodes).toEqual(first.nodes);
|
|
126
|
+
expect(second.members.map((m) => m.stamp)).toEqual(first.members.map((m) => m.stamp));
|
|
127
|
+
expect(second.members[0].stamp).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("an edit to one member reads that member again, and only it", async () => {
|
|
131
|
+
const root = workspace();
|
|
132
|
+
await workspaceGraph({ cwd: root });
|
|
133
|
+
writeFileSync(join(root, "services", "api", "ids.txt"), "Queue\nTopic\n");
|
|
134
|
+
age(root);
|
|
135
|
+
const g = result((await workspaceGraph({ cwd: root })).doc);
|
|
136
|
+
expect(g.nodes.map((n) => n.id)).toEqual(["api/Queue", "api/Topic", "web/Site"]);
|
|
137
|
+
expect(g.members.map((m) => [m.name, m.cached])).toEqual([
|
|
138
|
+
["api", false],
|
|
139
|
+
["web", true],
|
|
140
|
+
]);
|
|
141
|
+
expect(spawns(root).filter((p) => p.endsWith("services/api"))).toHaveLength(2);
|
|
142
|
+
expect(spawns(root).filter((p) => p.endsWith("apps/web"))).toHaveLength(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("an install change reads every member again", async () => {
|
|
146
|
+
const root = workspace();
|
|
147
|
+
await workspaceGraph({ cwd: root });
|
|
148
|
+
writeFileSync(join(root, "node_modules", ".package-lock.json"), "{}\n");
|
|
149
|
+
const g = result((await workspaceGraph({ cwd: root })).doc);
|
|
150
|
+
expect(g.members.every((m) => m.cached === false)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("--no-cache reads every member and writes nothing", async () => {
|
|
154
|
+
const root = workspace();
|
|
155
|
+
const g = result((await workspaceGraph({ cwd: root, noCache: true })).doc);
|
|
156
|
+
expect(g.members.every((m) => m.cached === false)).toBe(true);
|
|
157
|
+
expect(existsSync(graphCacheDir(root))).toBe(false);
|
|
158
|
+
await workspaceGraph({ cwd: root });
|
|
159
|
+
await workspaceGraph({ cwd: root, noCache: true });
|
|
160
|
+
expect(spawns(root)).toHaveLength(6);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a read of a file younger than the freshness window is not stored", async () => {
|
|
164
|
+
const root = workspace();
|
|
165
|
+
writeFileSync(join(root, "apps", "web", "ids.txt"), "Site\n");
|
|
166
|
+
await workspaceGraph({ cwd: root });
|
|
167
|
+
const g = result((await workspaceGraph({ cwd: root })).doc);
|
|
168
|
+
expect(g.members.map((m) => [m.name, m.cached])).toEqual([
|
|
169
|
+
["api", true],
|
|
170
|
+
["web", false],
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a stamp that moves during the read is not stored", async () => {
|
|
175
|
+
const root = workspace();
|
|
176
|
+
writeFileSync(join(root, "apps", "web", "race"), "");
|
|
177
|
+
age(root);
|
|
178
|
+
await workspaceGraph({ cwd: root });
|
|
179
|
+
age(root);
|
|
180
|
+
const g = result((await workspaceGraph({ cwd: root })).doc);
|
|
181
|
+
expect(g.members.find((m) => m.name === "web")?.cached).toBe(false);
|
|
182
|
+
expect(g.members.find((m) => m.name === "api")?.cached).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a failed member read is not stored", async () => {
|
|
186
|
+
const root = workspace();
|
|
187
|
+
writeFileSync(join(root, "apps", "web", "fail"), "");
|
|
188
|
+
age(root);
|
|
189
|
+
const one = result((await workspaceGraph({ cwd: root })).doc);
|
|
190
|
+
expect(one.members.find((m) => m.name === "web")).toMatchObject({ status: "failed", reason: { code: "command-failed" } });
|
|
191
|
+
unlinkSync(join(root, "apps", "web", "fail"));
|
|
192
|
+
age(root);
|
|
193
|
+
const two = result((await workspaceGraph({ cwd: root })).doc);
|
|
194
|
+
expect(two.members.find((m) => m.name === "web")).toMatchObject({ status: "composed", cached: false });
|
|
195
|
+
expect(two.members.find((m) => m.name === "api")?.cached).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("--at reads are keyed on the commit, so an edit after it still hits", async () => {
|
|
199
|
+
const root = workspace();
|
|
200
|
+
const sha = commitAll(root, "one");
|
|
201
|
+
const first = result((await workspaceGraph({ cwd: root, at: sha })).doc);
|
|
202
|
+
expectValid(first);
|
|
203
|
+
expect(first.members.every((m) => m.cached === false)).toBe(true);
|
|
204
|
+
writeFileSync(join(root, "services", "api", "ids.txt"), "Changed\n");
|
|
205
|
+
const before = spawns(root).length;
|
|
206
|
+
const second = result((await workspaceGraph({ cwd: root, at: sha })).doc);
|
|
207
|
+
expect(spawns(root)).toHaveLength(before);
|
|
208
|
+
expect(second.members.every((m) => m.cached === true)).toBe(true);
|
|
209
|
+
expect(second.nodes.map((n) => n.id)).toEqual(["api/Queue", "web/Site"]);
|
|
210
|
+
// The working tree is its own key.
|
|
211
|
+
const now = result((await workspaceGraph({ cwd: root })).doc);
|
|
212
|
+
expect(now.nodes.map((n) => n.id)).toEqual(["api/Changed", "web/Site"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a live command line is never looked up or stamped", () => {
|
|
216
|
+
const root = workspace();
|
|
217
|
+
const plan = planMembers("graph", root);
|
|
218
|
+
const cache = openGraphCache(root);
|
|
219
|
+
const args = {} as ParsedArgs;
|
|
220
|
+
const live = splitCached(plan, args, cache, null, () => ["graph", ".", "--format", "ir", "--live"]);
|
|
221
|
+
expect(live.hits).toEqual([]);
|
|
222
|
+
expect(live.pending.size).toBe(0);
|
|
223
|
+
expect(live.stamps.size).toBe(0);
|
|
224
|
+
const source = splitCached(plan, args, cache, null);
|
|
225
|
+
expect(source.pending.size).toBe(2);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("the cache's pieces", () => {
|
|
230
|
+
test("only a source read is cacheable", () => {
|
|
231
|
+
expect(isCacheableArgv(["graph", ".", "--format", "ir"])).toBe(true);
|
|
232
|
+
expect(isCacheableArgv(["graph", ".", "--format", "ir", "--env", "prod"])).toBe(true);
|
|
233
|
+
for (const f of ["--live", "--overlay", "--traffic", "--traffic=peak"]) expect(isCacheableArgv(["graph", ".", f])).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("the stamp skips node_modules, dist, .git and dot-directories, and member exclusions", () => {
|
|
237
|
+
const root = repo({ "a.ts": "", "sub/b.ts": "", "other/c.ts": "" });
|
|
238
|
+
const base = memberStamp(root, ["other"])!.value;
|
|
239
|
+
for (const path of ["node_modules/x.js", "dist/y.js", ".cache/z", "sub/node_modules/q.js", "other/c.ts"]) {
|
|
240
|
+
repoWrite(root, path, String(Math.random()));
|
|
241
|
+
expect(memberStamp(root, ["other"])!.value, path).toBe(base);
|
|
242
|
+
}
|
|
243
|
+
repoWrite(root, "sub/b.ts", "changed");
|
|
244
|
+
expect(memberStamp(root, ["other"])!.value).not.toBe(base);
|
|
245
|
+
expect(memberStamp(join(root, "missing"))).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("the environment digest ignores the volatile variables and nothing else", () => {
|
|
249
|
+
const base = { PATH: "/bin", HOME: "/h" };
|
|
250
|
+
const s = environmentStamp(base);
|
|
251
|
+
expect(environmentStamp({ ...base, PWD: "/x", SHLVL: "3", TERM_PROGRAM: "y", npm_config_x: "1" })).toBe(s);
|
|
252
|
+
expect(environmentStamp({ ...base, CHANT_ENV: "prod" })).not.toBe(s);
|
|
253
|
+
expect(VOLATILE_ENV).toContain("PWD");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("an entry is written whole: a torn or foreign file is a miss", () => {
|
|
257
|
+
const root = repo({});
|
|
258
|
+
const cache = openGraphCache(root);
|
|
259
|
+
cache.put({ format: 1, key: "k", member: "m", stamp: "s", chant: null, stdout: "{}" });
|
|
260
|
+
expect(cache.get("k")?.stdout).toBe("{}");
|
|
261
|
+
writeFileSync(join(cache.dir, "k.json"), '{"format":1,"key":"k"');
|
|
262
|
+
expect(cache.get("k")).toBeUndefined();
|
|
263
|
+
writeFileSync(join(cache.dir, "j.json"), JSON.stringify({ format: 1, key: "other", stdout: "{}" }));
|
|
264
|
+
expect(cache.get("j")).toBeUndefined();
|
|
265
|
+
expect(readdirSync(cache.dir).some((n) => n.endsWith(".tmp"))).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("each workspace has its own directory under the cache dir", () => {
|
|
269
|
+
const a = repo({});
|
|
270
|
+
const b = repo({});
|
|
271
|
+
expect(graphCacheDir(a, { CHANT_CACHE_DIR: "/c" })).toMatch(/^\/c\/workspace-graph\/[0-9a-f]{16}$/);
|
|
272
|
+
expect(graphCacheDir(a, { CHANT_CACHE_DIR: "/c" })).not.toBe(graphCacheDir(b, { CHANT_CACHE_DIR: "/c" }));
|
|
273
|
+
expect(graphCacheDir(a, { XDG_CACHE_HOME: "/x" })).toMatch(/^\/x\/chant\/workspace-graph\//);
|
|
274
|
+
expect(environmentStamp({ PATH: "/bin", CHANT_CACHE_DIR: "/one" })).toBe(environmentStamp({ PATH: "/bin", CHANT_CACHE_DIR: "/two" }));
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe("a member read through a kind's reader project (#2874)", () => {
|
|
279
|
+
/** Logs every graph it answers, and prints one node naming the directory it ran in. */
|
|
280
|
+
function readerChant(root: string): string {
|
|
281
|
+
return `#!/bin/sh
|
|
282
|
+
[ "$1" = graph ] || { echo "Error: Unknown command: $1" >&2; exit 1; }
|
|
283
|
+
echo "$PWD" >> "${root}/spawns.log"
|
|
284
|
+
printf '{"version":1,"nodes":[{"id":"root/Thing","kind":"Thing","lexicon":"fake","attrs":{}}],"edges":[],"groups":{}}\\n'
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const KIND = {
|
|
289
|
+
name: "tf",
|
|
290
|
+
description: "a root",
|
|
291
|
+
precedence: 400,
|
|
292
|
+
probe: { anyFile: ["*.tf"] },
|
|
293
|
+
graph: { lexicon: "fake", config: { roots: { "{member}": { dir: "{dir}" } } } },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function readerWorkspace(): string {
|
|
297
|
+
const root = repo({
|
|
298
|
+
"chant.workspace.json": declaration([{ name: "net", dir: "estates/net", kind: "tf" }], { pins: [{ package: "@intentius/chant-lexicon-fake", version: "1.0.0" }] }),
|
|
299
|
+
"estates/net/main.tf": "",
|
|
300
|
+
".gitignore": "node_modules\nspawns.log\n",
|
|
301
|
+
"node_modules/@intentius/chant-lexicon-fake/package.json": JSON.stringify({
|
|
302
|
+
name: "@intentius/chant-lexicon-fake",
|
|
303
|
+
version: "1.0.0",
|
|
304
|
+
exports: { ".": "./index.js", "./workspace-kinds": "./workspace-kinds.json" },
|
|
305
|
+
}),
|
|
306
|
+
"node_modules/@intentius/chant-lexicon-fake/workspace-kinds.json": JSON.stringify({ schema: 1, kinds: [KIND] }),
|
|
307
|
+
});
|
|
308
|
+
repoWrite(root, "node_modules/.bin/chant", readerChant(root), 0o755);
|
|
309
|
+
age(root);
|
|
310
|
+
return root;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
test("is cached on the member's own directory, not the temporary reader project", async () => {
|
|
314
|
+
const root = readerWorkspace();
|
|
315
|
+
const first = result((await workspaceGraph({ cwd: root })).doc);
|
|
316
|
+
expectValid(first);
|
|
317
|
+
expect(first.members[0]).toMatchObject({ name: "net", status: "composed", cached: false });
|
|
318
|
+
// It ran in a reader project outside the workspace.
|
|
319
|
+
expect(spawns(root)[0].startsWith(root)).toBe(false);
|
|
320
|
+
|
|
321
|
+
const second = result((await workspaceGraph({ cwd: root })).doc);
|
|
322
|
+
expect(second.members[0]).toMatchObject({ cached: true, stamp: first.members[0].stamp });
|
|
323
|
+
expect(spawns(root)).toHaveLength(1);
|
|
324
|
+
expect(second.nodes.map((n) => n.id)).toEqual(["net/root/Thing"]);
|
|
325
|
+
|
|
326
|
+
// The stamp is the member's: an edit in its directory reads it again.
|
|
327
|
+
writeFileSync(join(root, "estates", "net", "main.tf"), 'resource "x" "y" {}\n');
|
|
328
|
+
age(root);
|
|
329
|
+
const third = result((await workspaceGraph({ cwd: root })).doc);
|
|
330
|
+
expect(third.members[0].cached).toBe(false);
|
|
331
|
+
expect(third.members[0].stamp).not.toBe(first.members[0].stamp);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("a change to the kind's graph block reads the member again", async () => {
|
|
335
|
+
const root = readerWorkspace();
|
|
336
|
+
await workspaceGraph({ cwd: root });
|
|
337
|
+
const kinds = join(root, "node_modules", "@intentius", "chant-lexicon-fake", "workspace-kinds.json");
|
|
338
|
+
writeFileSync(kinds, JSON.stringify({ schema: 1, kinds: [{ ...KIND, graph: { lexicon: "fake", config: { callModuleType: "none", roots: { "{member}": { dir: "{dir}" } } } } }] }));
|
|
339
|
+
const g = result((await workspaceGraph({ cwd: root })).doc);
|
|
340
|
+
expect(g.members[0].cached).toBe(false);
|
|
341
|
+
expect(spawns(root)).toHaveLength(2);
|
|
342
|
+
});
|
|
343
|
+
});
|