@aefree/pi-plastic 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -7
  3. package/extensions/bash-cm-diff-guard.ts +2 -1
  4. package/extensions/file-discovery-filter.ts +67 -0
  5. package/index.ts +18 -2
  6. package/package.json +7 -8
  7. package/skills/_shared/references/conventional-commits.md +198 -198
  8. package/skills/using-plastic/SKILL.md +3 -3
  9. package/skills/using-plastic/references/changeset-operations.md +5 -5
  10. package/skills/using-plastic/references/code-review-creation.md +30 -30
  11. package/skills/using-plastic/references/integration.md +5 -3
  12. package/skills/using-plastic/references/quick-reference.md +2 -1
  13. package/skills/using-plastic/references/resources.md +7 -7
  14. package/skills/using-plastic/references/reviewing-changes.md +9 -7
  15. package/skills/using-plastic/references/shelving.md +25 -25
  16. package/skills/using-plastic/references/troubleshooting.md +5 -3
  17. package/skills/using-plastic/references/workspaces.md +33 -33
  18. package/src/file-discovery-filter-rendezvous.ts +104 -0
  19. package/src/{repository-search-provider.ts → file-discovery-filter.ts} +21 -17
  20. package/src/plastic-core.ts +579 -170
  21. package/src/plastic-tool-loading.ts +3 -3
  22. package/evals/tool-loading/README.md +0 -38
  23. package/evals/tool-loading/RESULTS.md +0 -57
  24. package/evals/tool-loading/baseline.json +0 -34
  25. package/evals/tool-loading/cases.json +0 -70
  26. package/evals/tool-loading/config.json +0 -19
  27. package/evals/tool-loading/mutation-guard.ts +0 -30
  28. package/evals/tool-loading/provider-capture.ts +0 -22
  29. package/evals/tool-loading/results/.gitignore +0 -3
  30. package/evals/tool-loading/run-eval.ts +0 -372
  31. package/extensions/repository-search-provider.ts +0 -66
  32. package/tests/bash-cm-diff-guard-validation.ts +0 -55
  33. package/tests/fixtures/plastic.selector +0 -3
  34. package/tests/fixtures/status-compact.txt +0 -1
  35. package/tests/fixtures/status-normal.txt +0 -1
  36. package/tests/packed-copy-owner-conflict.test.ts +0 -28
  37. package/tests/pi-tool-harness.ts +0 -116
  38. package/tests/plastic-branch-create-live.test.ts +0 -78
  39. package/tests/plastic-branch-query.test.ts +0 -26
  40. package/tests/plastic-branch-status.test.ts +0 -241
  41. package/tests/plastic-checkin-path-resolution.ts +0 -219
  42. package/tests/plastic-constrained-sampling.test.ts +0 -77
  43. package/tests/plastic-diff-workflow.test.ts +0 -86
  44. package/tests/plastic-dynamic-tool-loading.test.ts +0 -224
  45. package/tests/plastic-extension-registration.test.ts +0 -34
  46. package/tests/plastic-live-smoke.test.ts +0 -37
  47. package/tests/plastic-mutation-execution.test.ts +0 -61
  48. package/tests/plastic-patch-command.test.ts +0 -118
  49. package/tests/plastic-process-lifecycle.test.ts +0 -148
  50. package/tests/plastic-tool-rendering.test.ts +0 -16
  51. package/tests/plastic-tool-validation.ts +0 -153
  52. package/tests/plastic-workspace.test.ts +0 -62
  53. package/tests/repository-search-provider-registration.test.ts +0 -37
  54. package/tests/repository-search-provider.test.ts +0 -27
@@ -1,78 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { invokeRegisteredTool } from "./pi-tool-harness.ts";
3
-
4
- type ToolEnvelope = {
5
- ok?: boolean;
6
- data?: Record<string, unknown>;
7
- };
8
-
9
- const parseToolEnvelope = (text: string): ToolEnvelope => {
10
- const fencedJson = text.match(/```json\s*([\s\S]*?)\s*```/);
11
- assert(fencedJson, "Expected a structured JSON tool result.");
12
- return JSON.parse(fencedJson[1]) as ToolEnvelope;
13
- };
14
-
15
- const workspace = process.env.PI_PLASTIC_TEST_WORKSPACE?.trim();
16
- const mutationAllowed = process.env.PI_PLASTIC_ALLOW_MUTATION_TESTS?.trim().toLowerCase() === "true";
17
-
18
- if (!workspace || !mutationAllowed) {
19
- console.log("SKIP: set PI_PLASTIC_TEST_WORKSPACE and PI_PLASTIC_ALLOW_MUTATION_TESTS=true to run branch mutation tests");
20
- process.exit(0);
21
- }
22
-
23
- const status = parseToolEnvelope(await invokeRegisteredTool("plastic_status", { short: true, format: "json" }, workspace));
24
- assert.equal(status.ok, true, "Expected Plastic status to succeed.");
25
- const summary = status.data?.summary as Record<string, unknown> | undefined;
26
- const mergeState = status.data?.mergeState as Record<string, unknown> | undefined;
27
- assert.equal(summary?.totalPending, 0, "Live branch tests require a clean workspace.");
28
- assert.equal(mergeState?.hasMergeInProgress, false, "Live branch tests require no merge in progress.");
29
-
30
- const currentResult = parseToolEnvelope(await invokeRegisteredTool("plastic_currentBranch", { format: "json" }, workspace));
31
- const loadedBranch = currentResult.data?.branch;
32
- assert.equal(typeof loadedBranch, "string", "Expected a loaded Plastic branch.");
33
-
34
- const suffix = `${process.pid}-${Date.now().toString(36)}`;
35
- const parent = `${loadedBranch}/pi-live-parent-${suffix}`;
36
- const childLeaf = `child-${suffix}`;
37
- const child = `${parent}/${childLeaf}`;
38
- const blockedRoot = `/pi-live-root-${suffix}`;
39
-
40
- const exists = async (branch: string): Promise<boolean> =>
41
- (await invokeRegisteredTool("plastic_branchExists", { branch }, workspace)).trim() === "true";
42
-
43
- assert.equal(await exists(parent), false, `Temporary parent already exists: ${parent}`);
44
- assert.equal(await exists(child), false, `Temporary child already exists: ${child}`);
45
- assert.equal(await exists(blockedRoot), false, `Temporary root already exists: ${blockedRoot}`);
46
-
47
- try {
48
- await invokeRegisteredTool("plastic_branchCreate", { branch: parent, comment: "pi-plastic live parent test" }, workspace);
49
- assert.equal(await exists(parent), true, "Expected full hierarchical branch creation to succeed.");
50
-
51
- await invokeRegisteredTool("plastic_branchCreate", {
52
- branch: childLeaf,
53
- parent,
54
- comment: "pi-plastic explicit-parent live test",
55
- }, workspace);
56
- assert.equal(await exists(child), true, "Expected creation beneath an explicit non-loaded parent to succeed.");
57
-
58
- let rootGuarded = false;
59
- try {
60
- const output = await invokeRegisteredTool("plastic_branchCreate", { branch: blockedRoot }, workspace);
61
- rootGuarded = /Refusing to create top-level branch/.test(output);
62
- } catch (error) {
63
- rootGuarded = /Refusing to create top-level branch/.test(error instanceof Error ? error.message : String(error));
64
- }
65
- assert.equal(rootGuarded, true, "Expected top-level creation to be rejected without allowRootBranch.");
66
- assert.equal(await exists(blockedRoot), false, "Rejected top-level branch must not be created.");
67
-
68
- const branchAfter = parseToolEnvelope(await invokeRegisteredTool("plastic_currentBranch", { format: "json" }, workspace));
69
- assert.equal(branchAfter.data?.branch, loadedBranch, "Branch creation must not switch the workspace.");
70
- } finally {
71
- if (await exists(child)) await invokeRegisteredTool("plastic_branchDelete", { branch: child }, workspace);
72
- if (await exists(parent)) await invokeRegisteredTool("plastic_branchDelete", { branch: parent }, workspace);
73
- }
74
-
75
- assert.equal(await exists(child), false, "Temporary child branch cleanup failed.");
76
- assert.equal(await exists(parent), false, "Temporary parent branch cleanup failed.");
77
- assert.equal(await exists(blockedRoot), false, "Top-level guard cleanup invariant failed.");
78
- console.log("PASS: live hierarchical branch creation, explicit-parent behavior, top-level guard, and cleanup");
@@ -1,26 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { __plasticBranchInternals } from "../src/plastic-core.ts";
3
-
4
- const { getBranchLeafName, resolveBranchCreationTarget } = __plasticBranchInternals;
5
-
6
- assert.equal(getBranchLeafName("/main/feature/test"), "test");
7
- assert.equal(getBranchLeafName("br:/main/feature/test"), "test");
8
- assert.equal(getBranchLeafName("br:/main/feature/test@repo@server"), "test");
9
- assert.equal(getBranchLeafName("test"), "test");
10
-
11
- assert.equal(resolveBranchCreationTarget("foo", "/main"), "/main/foo");
12
- assert.equal(resolveBranchCreationTarget("feature/foo", "/main"), "/main/feature/foo");
13
- assert.equal(resolveBranchCreationTarget("foo", "/release"), "/release/foo");
14
- assert.equal(resolveBranchCreationTarget("/release/foo"), "/release/foo");
15
- assert.equal(resolveBranchCreationTarget("br:/release/foo@repo@server"), "br:/release/foo@repo@server");
16
- assert.equal(resolveBranchCreationTarget("/foo", undefined, true), "/foo");
17
- assert.throws(
18
- () => resolveBranchCreationTarget("/foo"),
19
- /Refusing to create top-level branch \/foo.*allowRootBranch=true/,
20
- );
21
- assert.throws(
22
- () => resolveBranchCreationTarget("foo"),
23
- /relative branch name requires a parent branch/,
24
- );
25
-
26
- console.log("PASS: Plastic branch query normalization tests passed");
@@ -1,241 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
- import { createPlasticBranchStatusExtension } from "../extensions/plastic-branch-status.ts";
4
-
5
- type Handler = (event: any, ctx: any) => Promise<void> | void;
6
- type FakeTimer = { callback: () => void; delay: number; cleared: boolean };
7
-
8
- class FooterHarness {
9
- handlers = new Map<string, Handler[]>();
10
- statuses = new Map<string, string>();
11
- statusCalls: Array<[string, string | undefined]> = [];
12
- setFooterCalls = 0;
13
- readonly ctx = {
14
- cwd: "/workspace/nested/project",
15
- hasUI: true,
16
- ui: {
17
- theme: { fg: (color: string, text: string) => `<${color}>${text}</${color}>` },
18
- setStatus: (key: string, value: string | undefined) => {
19
- this.statusCalls.push([key, value]);
20
- if (value === undefined) this.statuses.delete(key);
21
- else this.statuses.set(key, value);
22
- },
23
- setFooter: () => { this.setFooterCalls++; },
24
- },
25
- };
26
- readonly api = {
27
- on: (event: string, handler: Handler) => {
28
- const handlers = this.handlers.get(event) ?? [];
29
- handlers.push(handler);
30
- this.handlers.set(event, handlers);
31
- },
32
- exec: async () => ({ stdout: "", stderr: "", code: 0, killed: false }),
33
- };
34
-
35
- async emit(event: string, payload: any = {}): Promise<void> {
36
- for (const handler of this.handlers.get(event) ?? []) await handler(payload, this.ctx);
37
- }
38
- }
39
-
40
- const workspace = (root: string) => ({
41
- kind: "found" as const,
42
- value: { root, plasticDir: `${root}/.plastic`, markerPath: `${root}/.plastic/plastic.workspace` },
43
- });
44
-
45
- const timers: FakeTimer[] = [];
46
- const schedule = (callback: () => void, delay: number): any => {
47
- const timer = { callback, delay, cleared: false };
48
- timers.push(timer);
49
- return timer;
50
- };
51
- const clear = (timer: any): void => { timer.cleared = true; };
52
- const runTimers = async (delay: number): Promise<void> => {
53
- const pending = timers.filter((timer) => !timer.cleared && timer.delay === delay);
54
- for (const timer of pending) {
55
- timer.cleared = true;
56
- timer.callback();
57
- }
58
- await new Promise((resolve) => setImmediate(resolve));
59
- };
60
-
61
- {
62
- timers.length = 0;
63
- const harness = new FooterHarness();
64
- harness.statuses.set("foreign-status", "keep me");
65
- let confirmation = "/main/from-cm@repo@server\n";
66
- let confirmCount = 0;
67
- let watcherClosed = 0;
68
- let watcherChange: (() => void) | undefined;
69
- let watcherError: (() => void) | undefined;
70
-
71
- createPlasticBranchStatusExtension({
72
- discoverWorkspace: async (cwd) => cwd.startsWith("/sibling") ? workspace("/sibling") : workspace("/workspace"),
73
- readSelector: async () => "repository repo@server\n path /\n smartbranch /main/from-selector\n",
74
- watchDirectory: (_path, onChange, onError) => {
75
- watcherChange = onChange;
76
- watcherError = onError;
77
- return { close: () => { watcherClosed++; } };
78
- },
79
- confirmBranch: async () => { confirmCount++; return confirmation; },
80
- setTimeout: schedule,
81
- clearTimeout: clear,
82
- platform: "linux",
83
- })(harness.api as any);
84
-
85
- await harness.emit("session_start", { reason: "startup" });
86
- assert.match(harness.statuses.get("plastic-branch") ?? "", /\/main\/from-cm/);
87
- assert.equal(harness.statuses.get("foreign-status"), "keep me");
88
- assert.equal(harness.setFooterCalls, 0);
89
- assert.equal(confirmCount, 1);
90
-
91
- confirmation = "/main/after-selector-replacement@repo@server\n";
92
- watcherChange?.();
93
- watcherChange?.();
94
- await runTimers(150);
95
- assert.equal(confirmCount, 2, "coalesced watcher events should run one refresh");
96
- assert.match(harness.statuses.get("plastic-branch") ?? "", /after-selector-replacement/);
97
- assert.ok(watcherClosed >= 1, "workspace watcher should be rebound after a directory event");
98
-
99
- await harness.emit("tool_result", { toolCallId: "same", toolName: "plastic_switchBranch", input: { workdir: "." }, isError: false });
100
- await harness.emit("tool_execution_end", { toolCallId: "same", toolName: "plastic_switchBranch", isError: false });
101
- await runTimers(150);
102
- assert.equal(confirmCount, 3, "same-workspace Plastic tools should refresh");
103
-
104
- await harness.emit("tool_result", { toolCallId: "sibling", toolName: "plastic_status", input: { workdir: "/sibling" }, isError: false });
105
- await harness.emit("tool_execution_end", { toolCallId: "sibling", toolName: "plastic_status", isError: false });
106
- await runTimers(150);
107
- assert.equal(confirmCount, 3, "sibling-workspace tools must not refresh this footer");
108
-
109
- await harness.emit("tool_result", { toolCallId: "sibling-alias", toolName: "plastic_status", input: { cwd: "/sibling", workdir: "/sibling" }, isError: false });
110
- await harness.emit("tool_execution_end", { toolCallId: "sibling-alias", toolName: "plastic_status", isError: false });
111
- await runTimers(150);
112
- assert.equal(confirmCount, 3, "normalized effective workdir should control sibling filtering");
113
-
114
- watcherError?.();
115
- await runTimers(150);
116
- assert.equal(confirmCount, 4, "watcher errors should trigger bounded revalidation");
117
-
118
- await harness.emit("session_shutdown", { reason: "reload" });
119
- assert.equal(harness.statuses.has("plastic-branch"), false);
120
- assert.equal(harness.statuses.get("foreign-status"), "keep me");
121
- }
122
-
123
- {
124
- timers.length = 0;
125
- const harness = new FooterHarness();
126
- createPlasticBranchStatusExtension({
127
- discoverWorkspace: async () => workspace("/workspace"),
128
- readSelector: async () => "smartbranch /main/selector-only\n",
129
- watchDirectory: () => ({ close() {} }),
130
- confirmBranch: async () => { throw new Error("cm unavailable"); },
131
- setTimeout: schedule,
132
- clearTimeout: clear,
133
- platform: "linux",
134
- })(harness.api as any);
135
- await harness.emit("session_start");
136
- assert.match(harness.statuses.get("plastic-branch") ?? "", /selector-only/, "selector fallback should survive cm failure");
137
- }
138
-
139
- {
140
- timers.length = 0;
141
- const harness = new FooterHarness();
142
- let aborted = false;
143
- createPlasticBranchStatusExtension({
144
- discoverWorkspace: async () => workspace("/workspace"),
145
- readSelector: async () => "smartbranch \n",
146
- watchDirectory: () => ({ close() {} }),
147
- confirmBranch: async (_cwd, signal) => new Promise((_resolve, reject) => {
148
- signal.addEventListener("abort", () => { aborted = true; reject(new Error("aborted")); }, { once: true });
149
- }),
150
- setTimeout: schedule,
151
- clearTimeout: clear,
152
- platform: "win32",
153
- })(harness.api as any);
154
- const startup = harness.emit("session_start");
155
- await new Promise((resolve) => setImmediate(resolve));
156
- assert.match(harness.statuses.get("plastic-branch") ?? "", /unavailable/);
157
- await harness.emit("session_shutdown", { reason: "quit" });
158
- await startup;
159
- assert.equal(aborted, true, "shutdown should abort in-flight cm confirmation");
160
- assert.equal(harness.statuses.has("plastic-branch"), false);
161
- }
162
-
163
- {
164
- timers.length = 0;
165
- const harness = new FooterHarness();
166
- let markerPresent = true;
167
- const watched: Array<{ path: string; change: () => void; closed: boolean }> = [];
168
- createPlasticBranchStatusExtension({
169
- discoverWorkspace: async () => markerPresent ? workspace("/workspace") : { kind: "not_found" },
170
- readSelector: async () => "smartbranch /main/recreated\n",
171
- watchDirectory: (path, onChange) => {
172
- const handle = { path, change: onChange, closed: false };
173
- watched.push(handle);
174
- return { close: () => { handle.closed = true; } };
175
- },
176
- confirmBranch: async () => "/main/recreated@repo@server\n",
177
- setTimeout: schedule,
178
- clearTimeout: clear,
179
- platform: "linux",
180
- })(harness.api as any);
181
-
182
- await harness.emit("session_start");
183
- markerPresent = false;
184
- watched.find((entry) => entry.path.endsWith("/.plastic") && !entry.closed)?.change();
185
- await runTimers(150);
186
- assert.equal(harness.statuses.has("plastic-branch"), false);
187
- const recoveryWatcher = watched.find((entry) => entry.path === "/workspace" && !entry.closed);
188
- assert.ok(recoveryWatcher, "workspace root should remain watched while .plastic is temporarily absent");
189
-
190
- markerPresent = true;
191
- recoveryWatcher?.change();
192
- await runTimers(150);
193
- assert.match(harness.statuses.get("plastic-branch") ?? "", /recreated/);
194
- assert.ok(watched.some((entry) => entry.path.endsWith("/.plastic") && !entry.closed), "recreated .plastic directory should be rebound");
195
- await harness.emit("session_shutdown");
196
- assert.ok(watched.every((entry) => entry.closed), "all primary and recovery watchers should close on shutdown");
197
- }
198
-
199
- {
200
- timers.length = 0;
201
- const harness = new FooterHarness();
202
- let innerPresent = true;
203
- const watched: Array<{ path: string; change: () => void; closed: boolean }> = [];
204
- createPlasticBranchStatusExtension({
205
- discoverWorkspace: async () => innerPresent ? workspace("/workspace/inner") : workspace("/workspace"),
206
- readSelector: async (path) => path.includes("/inner/") ? "smartbranch /main/inner\n" : "smartbranch /main/outer\n",
207
- watchDirectory: (path, onChange) => {
208
- const handle = { path, change: onChange, closed: false };
209
- watched.push(handle);
210
- return { close: () => { handle.closed = true; } };
211
- },
212
- confirmBranch: async (cwd) => cwd.endsWith("/inner") ? "/main/inner@repo@server\n" : "/main/outer@repo@server\n",
213
- setTimeout: schedule,
214
- clearTimeout: clear,
215
- platform: "linux",
216
- })(harness.api as any);
217
-
218
- await harness.emit("session_start");
219
- innerPresent = false;
220
- watched.find((entry) => entry.path === "/workspace/inner/.plastic" && !entry.closed)?.change();
221
- await runTimers(150);
222
- assert.match(harness.statuses.get("plastic-branch") ?? "", /\/main\/outer/);
223
- const nestedRecovery = watched.find((entry) => entry.path === "/workspace/inner" && !entry.closed);
224
- assert.ok(nestedRecovery, "former nested workspace root should remain watched during outer fallback");
225
-
226
- innerPresent = true;
227
- nestedRecovery?.change();
228
- await runTimers(150);
229
- assert.match(harness.statuses.get("plastic-branch") ?? "", /\/main\/inner/);
230
- assert.ok(watched.some((entry) => entry.path === "/workspace/inner/.plastic" && !entry.closed));
231
- assert.equal(watched.some((entry) => entry.path === "/workspace/inner" && !entry.closed), false);
232
- await harness.emit("session_shutdown");
233
- assert.ok(watched.every((entry) => entry.closed));
234
- }
235
-
236
- {
237
- const source = await readFile(new URL("../extensions/plastic-branch-status.ts", import.meta.url), "utf8");
238
- assert.doesNotMatch(source, /\.setFooter\s*\(/, "additive branch extension must never replace Pi's footer");
239
- }
240
-
241
- console.log("PASS: Plastic branch footer lifecycle and composition tests passed");
@@ -1,219 +0,0 @@
1
- import { __plasticCheckinInternals, __plasticSwitchInternals } from "../src/plastic-core.ts";
2
-
3
- const assert = (condition: boolean, message: string): void =>
4
- {
5
- if (!condition)
6
- {
7
- throw new Error(message);
8
- }
9
- };
10
-
11
- const normalizePath = (pathValue: string): string => pathValue.replace(/\\/g, "/").replace(/^[A-Za-z]:/, "");
12
- const pathMatches = (actualPath: string, expectedPath: string): boolean =>
13
- {
14
- const actual = normalizePath(actualPath);
15
- const expected = normalizePath(expectedPath);
16
- return actual === expected || actual.endsWith(`/${expected}`);
17
- };
18
-
19
- const main = (): void =>
20
- {
21
- const cwd = "/repo/ws";
22
- const machineOutput = [
23
- "STATUS 123 Normandie/Repo server",
24
- "CH /repo/ws/Assets/Gameplay/Movement.cs False NO_MERGES",
25
- "LD /repo/ws/Assets/Gameplay/FakeModifierService.cs False NO_MERGES",
26
- "MV /repo/ws/Assets/Gameplay/MockModifierService.cs False NO_MERGES",
27
- "CH /repo/ws/ProjectSettings/ProjectSettings.asset False NO_MERGES",
28
- ].join("\n");
29
-
30
- const pendingItems = __plasticCheckinInternals.parseMachineReadablePendingItems(machineOutput, cwd);
31
- assert(pendingItems.length === 4, "Expected parser to return four pending items.");
32
- assert(pendingItems.some((item) => item.kind === "deleted"), "Expected at least one deleted pending item.");
33
- assert(pendingItems.some((item) => item.kind === "moved"), "Expected at least one moved pending item.");
34
- assert(__plasticCheckinInternals.inferPendingItemKind("CO+RP") === "changed", "Expected CO+RP to classify as changed.");
35
-
36
- const pendingSummary = __plasticCheckinInternals.summarizePendingItems(pendingItems, cwd);
37
- assert(pendingSummary.totalPending === 4, "Expected pending summary total count to match parsed items.");
38
- assert(pendingSummary.tracked === 4, "Expected tracked count to equal total when no private items exist.");
39
- assert(pendingSummary.private === 0, "Expected no private items in initial sample summary.");
40
-
41
- const changedOnly = __plasticCheckinInternals.resolveCheckinPaths([
42
- "Assets/Gameplay/Movement.cs",
43
- ], pendingItems, cwd);
44
- assert(changedOnly.includedPaths.length === 1, "Expected one included path for changed file scope.");
45
- assert(pathMatches(changedOnly.includedPaths[0], "Assets/Gameplay/Movement.cs"), "Expected changed file path to be preserved.");
46
- assert(changedOnly.shouldApplyChanged === false, "Expected changed-only scope to avoid auto --applychanged.");
47
-
48
- const deletedOnly = __plasticCheckinInternals.resolveCheckinPaths([
49
- "Assets/Gameplay/FakeModifierService.cs",
50
- ], pendingItems, cwd);
51
- assert(deletedOnly.includedPaths.length === 1, "Expected one included path for deleted file scope.");
52
- assert(pathMatches(deletedOnly.includedPaths[0], "Assets/Gameplay/FakeModifierService.cs"), "Expected deleted-file scope to preserve the requested path before fallback retry.");
53
- assert(deletedOnly.shouldApplyChanged, "Expected deleted-file scope to enable --applychanged.");
54
- assert(deletedOnly.fallbackPaths.length === 1 && pathMatches(deletedOnly.fallbackPaths[0], "Assets/Gameplay"),
55
- "Expected deleted-file fallback scope to target the parent directory.");
56
-
57
- const directoryScope = __plasticCheckinInternals.resolveCheckinPaths([
58
- "Assets/Gameplay",
59
- ], pendingItems, cwd);
60
- assert(directoryScope.includedPaths.length === 1, "Expected one included path for directory scope.");
61
- assert(pathMatches(directoryScope.includedPaths[0], "Assets/Gameplay"), "Expected directory scope to remain stable.");
62
- assert(directoryScope.shouldApplyChanged, "Expected directory scope with deleted/moved items to enable --applychanged.");
63
-
64
- const noMatch = __plasticCheckinInternals.resolveCheckinPaths([
65
- "Assets/UI/Nope.cs",
66
- ], pendingItems, cwd);
67
- assert(noMatch.includedPaths.length === 0, "Expected no included paths for unmatched scope.");
68
- assert(noMatch.excludedPaths.length === 1, "Expected one excluded path for unmatched scope.");
69
- assert(noMatch.excludedPaths[0].reason === "no_pending_changes", "Expected unmatched scope reason to be no_pending_changes.");
70
-
71
- const windowsCwd = "C:/Workspaces/GameProject/ws3";
72
- const windowsMachineOutput = [
73
- "STATUS 1147 ExampleRepo/game-unity example@server",
74
- "CH c:/Workspaces/GameProject/ws3/game-unity/Assets/Game/code/Runtime/Paths.cs False NO_MERGES",
75
- "CH c:/Workspaces/GameProject/ws3/game-unity/Assets/Game/code/Editor/Validation/MapGraphValidationBatchTools.cs False NO_MERGES",
76
- ].join("\n");
77
- const windowsPendingItems = __plasticCheckinInternals.parseMachineReadablePendingItems(windowsMachineOutput, windowsCwd);
78
- assert(windowsPendingItems.length === 2, "Expected Windows parser sample to return two pending items.");
79
-
80
- const windowsRelativeScope = __plasticCheckinInternals.resolveCheckinPaths([
81
- "game-unity/Assets/Game/code/Runtime/Paths.cs",
82
- ], windowsPendingItems, windowsCwd);
83
- assert(windowsRelativeScope.includedPaths.length === 1, "Expected Windows relative scope to match pending file despite drive-letter case mismatch.");
84
- assert(pathMatches(windowsRelativeScope.includedPaths[0], "game-unity/Assets/Game/code/Runtime/Paths.cs"), "Expected Windows relative scope to keep workspace-relative command path.");
85
-
86
- const windowsAbsoluteScope = __plasticCheckinInternals.resolveCheckinPaths([
87
- "C:/Workspaces/GameProject/ws3/game-unity/Assets/Game/code/Runtime/Paths.cs",
88
- ], windowsPendingItems, windowsCwd);
89
- assert(windowsAbsoluteScope.includedPaths.length === 1, "Expected Windows absolute scope to match pending file despite drive-letter case mismatch.");
90
- assert(pathMatches(windowsAbsoluteScope.includedPaths[0], "game-unity/Assets/Game/code/Runtime/Paths.cs"), "Expected Windows absolute scope to collapse back to workspace-relative command path.");
91
-
92
- const windowsDirectoryScope = __plasticCheckinInternals.resolveCheckinPaths([
93
- "GAME-UNITY/Assets/Game/code/Editor",
94
- ], windowsPendingItems, windowsCwd);
95
- assert(windowsDirectoryScope.includedPaths.length === 1, "Expected Windows directory scope to match pending children despite path case differences.");
96
- assert(pathMatches(windowsDirectoryScope.includedPaths[0], "GAME-UNITY/Assets/Game/code/Editor"), "Expected Windows directory scope to preserve the requested command path casing.");
97
-
98
- const windowsDuplicateScope = __plasticCheckinInternals.resolveCheckinPaths([
99
- "GAME-UNITY/Assets/Game/code/Runtime/Paths.cs",
100
- "game-unity/Assets/Game/code/Runtime/Paths.cs",
101
- ], windowsPendingItems, windowsCwd);
102
- assert(windowsDuplicateScope.includedPaths.length === 1, "Expected Windows duplicate scopes that differ only by case to dedupe to one command path.");
103
-
104
- assert(__plasticCheckinInternals.isSameFilesystemDevice(100, 100), "Expected Darwin case probing to remain on the target filesystem device.");
105
- assert(!__plasticCheckinInternals.isSameFilesystemDevice(100, 200), "Expected Darwin case probing to stop before crossing a mount-device boundary.");
106
-
107
- const comparisonPath = "/Volumes/Workspace/Assets/Player.cs";
108
- const darwinInsensitiveKey = __plasticCheckinInternals.toPathComparisonKeyFromAbsolutePath(comparisonPath, {
109
- platform: "darwin",
110
- isDarwinCaseInsensitiveVolume: () => true,
111
- });
112
- const darwinSensitiveKey = __plasticCheckinInternals.toPathComparisonKeyFromAbsolutePath(comparisonPath, {
113
- platform: "darwin",
114
- isDarwinCaseInsensitiveVolume: () => false,
115
- });
116
- assert(darwinInsensitiveKey === comparisonPath.toLowerCase(), "Expected a detected case-insensitive Darwin volume to normalize comparison keys.");
117
- assert(darwinSensitiveKey === comparisonPath, "Expected a case-sensitive APFS volume to preserve comparison-key casing.");
118
- assert(__plasticCheckinInternals.toPathComparisonKeyFromAbsolutePath("C:/Workspace/Assets/Player.cs", {
119
- platform: "darwin",
120
- isDarwinCaseInsensitiveVolume: () => false,
121
- }) === "c:/workspace/assets/player.cs", "Expected Windows drive paths to remain case-insensitive on every host platform.");
122
-
123
- const gameplayScope = pendingItems.find((item) => item.workspacePath.endsWith("Movement.cs"))?.normalizedPath.replace(/\/Movement\.cs$/, "") ?? "";
124
- const scopedPendingItems = __plasticCheckinInternals.filterPendingItemsByScope(
125
- pendingItems,
126
- gameplayScope.length > 0 ? [gameplayScope] : [],
127
- );
128
- assert(scopedPendingItems.length === 3, "Expected scope-filter helper to return only pending items under the requested path.");
129
-
130
- const fallbackPaths = __plasticCheckinInternals.buildFallbackScopePaths([
131
- "/repo/ws/Assets/Gameplay/Movement.cs",
132
- "/repo/ws/Assets/Gameplay/FakeModifierService.cs",
133
- ], cwd);
134
- assert(fallbackPaths.length === 1 && pathMatches(fallbackPaths[0], "Assets/Gameplay"), "Expected fallback paths to collapse to the common parent scope.");
135
-
136
- const normalizedDiffRevision = __plasticCheckinInternals.normalizeDiffFileRevisionSpec(
137
- "Assets/Gameplay/Movement.cs",
138
- "cs:947",
139
- );
140
- assert(normalizedDiffRevision === "Assets/Gameplay/Movement.cs#cs:947", "Expected diff-file revision shorthand to be expanded with path scope.");
141
-
142
- const numericDiffRevision = __plasticCheckinInternals.normalizeDiffFileRevisionSpec(
143
- "Assets/Gameplay/Movement.cs",
144
- "947",
145
- );
146
- assert(numericDiffRevision === "Assets/Gameplay/Movement.cs#cs:947", "Expected numeric diff-file revision shorthand to normalize to cs: selector.");
147
-
148
- const scopedRevision = __plasticCheckinInternals.normalizeDiffFileRevisionSpec(
149
- "Assets/Gameplay/Movement.cs",
150
- "Assets/Gameplay/Movement.cs#cs:947",
151
- );
152
- assert(scopedRevision === "Assets/Gameplay/Movement.cs#cs:947", "Expected already scoped revision to remain unchanged.");
153
-
154
- assert(__plasticCheckinInternals.isUnscopedDiffRevisionSpec("cs:947"), "Expected cs selector without file scope to be detected as unscoped.");
155
- assert(__plasticCheckinInternals.isUnscopedDiffRevisionSpec("947"), "Expected numeric selector without file scope to be detected as unscoped.");
156
- assert(!__plasticCheckinInternals.isUnscopedDiffRevisionSpec("Assets/Gameplay/Movement.cs#cs:947"), "Expected file-qualified selector not to be treated as unscoped.");
157
-
158
- assert(__plasticCheckinInternals.isGitUnknownOptionLabelError("error: unknown option `label'"), "Expected backtick-style git unknown-option error to be detected.");
159
- assert(__plasticCheckinInternals.isGitUnknownOptionLabelError("error: unknown option 'label'"), "Expected single-quote-style git unknown-option error to be detected.");
160
- assert(!__plasticCheckinInternals.isGitUnknownOptionLabelError("fatal: unrelated error"), "Expected unrelated git errors not to match label-option detector.");
161
-
162
- assert(__plasticCheckinInternals.isRevisionNotFoundError("The specified revision was not found foo#cs:1"), "Expected revision-not-found detector to match Plastic cat errors.");
163
- assert(!__plasticCheckinInternals.isRevisionNotFoundError("fatal: unrelated error"), "Expected unrelated errors not to match revision-not-found detector.");
164
-
165
- const branchSelectorFromRevision = __plasticCheckinInternals.extractBranchSelectorFromRevision("Assets/Gameplay/Movement.cs#br:/dev/task-123");
166
- assert(branchSelectorFromRevision === "br:/dev/task-123", "Expected branch selector extraction from file-qualified revision.");
167
- assert(__plasticCheckinInternals.extractBranchSelectorFromRevision("Assets/Gameplay/Movement.cs#cs:947") === null, "Expected no branch selector extraction for non-branch revisions.");
168
-
169
- const branchName = __plasticCheckinInternals.extractBranchNameFromSelector("br:/dev/task-123@Repo@server");
170
- assert(branchName === "/dev/task-123", "Expected branch selector repository suffix to be removed.");
171
- assert(__plasticCheckinInternals.extractBranchNameFromSelector("br:") === null, "Expected invalid branch selector to return null branch name.");
172
-
173
- assert(__plasticSwitchInternals.normalizeBranchSpecForComparison("br:/dev/task-123@Repo@server") === "/dev/task-123", "Expected switch branch normalization to remove br: prefix and repository suffix.");
174
- assert(__plasticSwitchInternals.isSameBranchSpec("/dev/task-123", "br:/dev/task-123@Repo@server"), "Expected branch comparison helper to treat equivalent branch specs as equal.");
175
- assert(!__plasticSwitchInternals.isSameBranchSpec("/dev/task-123", "/dev/task-124"), "Expected branch comparison helper to detect different branches.");
176
-
177
- const machineOutputWithPrivate = [
178
- "STATUS 123 Normandie/Repo server",
179
- "PR /repo/ws/todos/059-ready.md False NO_MERGES",
180
- "PR /repo/ws/.env.local False NO_MERGES",
181
- "PR /repo/ws/src/id_rsa False NO_MERGES",
182
- ].join("\n");
183
- const pendingItemsWithPrivate = __plasticCheckinInternals.parseMachineReadablePendingItems(machineOutputWithPrivate, cwd);
184
- const privateSummary = __plasticCheckinInternals.summarizePendingItems(pendingItemsWithPrivate, cwd);
185
- assert(privateSummary.private === 3, "Expected private summary to count private pending items.");
186
- assert(privateSummary.tracked === 0, "Expected tracked count to be zero for private-only sample.");
187
- assert(privateSummary.privatePaths.some((path) => pathMatches(path, "todos/059-ready.md")), "Expected summary private paths to include the expected markdown file.");
188
-
189
- const privateOnlyProfile = __plasticSwitchInternals.buildSwitchPendingProfile(privateSummary);
190
- assert(privateOnlyProfile.hasPrivateOnlyPendingChanges, "Expected switch pending profile to classify private-only pending state.");
191
- assert(__plasticSwitchInternals.canSwitchDirectWithPrivateOnlyPending("bring", false, privateOnlyProfile), "Expected private-only bring policy to allow direct unattended switch.");
192
- assert(__plasticSwitchInternals.canSwitchDirectWithPrivateOnlyPending("shelve", false, privateOnlyProfile), "Expected private-only shelve policy to allow direct unattended switch.");
193
- assert(__plasticSwitchInternals.canSwitchDirectWithPrivateOnlyPending("cancel", true, privateOnlyProfile), "Expected defaulted cancel policy to allow direct private-only switch.");
194
- assert(!__plasticSwitchInternals.canSwitchDirectWithPrivateOnlyPending("cancel", false, privateOnlyProfile), "Expected explicit cancel policy to keep private-only switch blocked.");
195
-
196
- assert(__plasticCheckinInternals.getSensitivePrivatePathReason(".env.local") === "sensitive_path:dotenv", "Expected dotenv path to be blocked as sensitive.");
197
- assert(__plasticCheckinInternals.getSensitivePrivatePathReason("src/id_rsa") === "sensitive_path:ssh-key", "Expected SSH key path to be blocked as sensitive.");
198
- assert(__plasticCheckinInternals.getSensitivePrivatePathReason("todos/059-ready.md") === null, "Expected normal markdown path not to be blocked as sensitive.");
199
-
200
- const autoAddSelection = __plasticCheckinInternals.selectPrivatePathsForAutoAdd(pendingItemsWithPrivate, [], cwd);
201
- assert(autoAddSelection.candidatePaths.length === 1, "Expected one safe private auto-add candidate.");
202
- assert(pathMatches(autoAddSelection.candidatePaths[0], "todos/059-ready.md"), "Expected private auto-add candidate to keep relative file path.");
203
- assert(autoAddSelection.blockedPaths.length === 2, "Expected two sensitive private paths to be blocked from auto-add.");
204
-
205
- const trackedPendingProfile = __plasticSwitchInternals.buildSwitchPendingProfile(pendingSummary);
206
- assert(__plasticSwitchInternals.isSwitchBringBlockedForUnattended("bring", trackedPendingProfile), "Expected bring policy to be blocked when tracked pending changes exist.");
207
- assert(!__plasticSwitchInternals.isSwitchBringBlockedForUnattended("bring", privateOnlyProfile), "Expected bring policy not to be blocked for private-only pending changes.");
208
-
209
- const legacyPendingSummary = __plasticSwitchInternals.toLegacyPendingSummary(privateSummary);
210
- assert(legacyPendingSummary.totalPending === privateSummary.totalPending, "Expected legacy pending summary total count to match detailed summary.");
211
- assert(legacyPendingSummary.other >= privateSummary.private, "Expected legacy pending summary to account for private items in other count.");
212
-
213
- assert(__plasticCheckinInternals.isNoChangesWorkspaceCheckinError("Error: There are no changes in the workspace c:/repo/ws"), "Expected no-changes detector to match workspace no-changes errors.");
214
- assert(!__plasticCheckinInternals.isNoChangesWorkspaceCheckinError("Error: path is not changed in current workspace"), "Expected no-changes detector not to match path-scope errors.");
215
-
216
- console.log("PASS: plastic checkin path resolution tests passed");
217
- };
218
-
219
- main();
@@ -1,77 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { loadRegisteredTools } from "./pi-tool-harness.ts";
3
-
4
- type JsonSchema = {
5
- type?: string;
6
- properties?: Record<string, JsonSchema>;
7
- required?: string[];
8
- additionalProperties?: boolean | JsonSchema;
9
- items?: JsonSchema;
10
- anyOf?: JsonSchema[];
11
- };
12
-
13
- const collectOpenAiStrictIssues = (schema: JsonSchema, path = "$", issues: string[] = []): string[] => {
14
- if (schema.type === "object") {
15
- if (schema.additionalProperties !== false) {
16
- issues.push(`${path} must set additionalProperties to false`);
17
- }
18
-
19
- const propertyNames = Object.keys(schema.properties ?? {});
20
- const required = new Set(schema.required ?? []);
21
- for (const propertyName of propertyNames) {
22
- if (!required.has(propertyName)) {
23
- issues.push(`${path}.${propertyName} is optional`);
24
- }
25
- collectOpenAiStrictIssues(schema.properties![propertyName], `${path}.${propertyName}`, issues);
26
- }
27
- }
28
-
29
- if (schema.items) {
30
- collectOpenAiStrictIssues(schema.items, `${path}[]`, issues);
31
- }
32
- for (const [index, variant] of (schema.anyOf ?? []).entries()) {
33
- collectOpenAiStrictIssues(variant, `${path}.anyOf[${index}]`, issues);
34
- }
35
-
36
- return issues;
37
- };
38
-
39
- const main = async (): Promise<void> => {
40
- const tools = await loadRegisteredTools();
41
- const report = [...tools.values()].map((tool) => {
42
- const schema = tool.parameters as JsonSchema;
43
- return {
44
- name: tool.name,
45
- issues: collectOpenAiStrictIssues(schema),
46
- constrainedSampling: tool.constrainedSampling,
47
- };
48
- });
49
-
50
- assert(report.length > 0, "Expected registered Plastic tools to audit.");
51
-
52
- const optedIn = report.filter((entry) => entry.constrainedSampling !== undefined && entry.constrainedSampling !== false);
53
- for (const entry of optedIn) {
54
- if (entry.constrainedSampling && entry.constrainedSampling.type === "json_schema") {
55
- assert.deepEqual(entry.issues, [], `${entry.name} must be OpenAI strict-schema compatible before opting in`);
56
- }
57
- }
58
-
59
- // Pi 0.82 forwards a strict tool's schema without normalizing it. Every current
60
- // Plastic schema has optional fields (at minimum workdir), and TypeBox objects
61
- // currently allow unspecified additional properties. Enabling strict sampling
62
- // would therefore make direct OpenAI requests fail or require a breaking public
63
- // argument-schema redesign.
64
- assert.equal(optedIn.length, 0, "No Plastic tool should opt in until its ordinary public schema is strict-compatible.");
65
- assert.equal(report.filter((entry) => entry.issues.length === 0).length, 0, "The current catalog should be classified as blocked, not strict-ready.");
66
-
67
- for (const pilotName of ["plastic_status", "plastic_branchExists", "plastic_branchCreate"]) {
68
- const pilot = report.find((entry) => entry.name === pilotName);
69
- assert(pilot, `Missing pilot tool ${pilotName}`);
70
- assert(pilot.issues.some((issue) => issue.includes("additionalProperties")), `${pilotName} should report its closed-object blocker`);
71
- assert(pilot.issues.some((issue) => issue.endsWith("workdir is optional")), `${pilotName} should report its optional workdir blocker`);
72
- }
73
-
74
- console.log(`PASS: audited ${report.length} Plastic schemas; 0 are currently OpenAI strict-ready`);
75
- };
76
-
77
- void main();