@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,86 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { __plasticDiffInternals, __plasticProcessInternals } from "../src/plastic-core.ts";
6
-
7
- const root = await mkdtemp(join(tmpdir(), "pi-plastic-diff-"));
8
-
9
- try {
10
- const { resolveDiffFileRevision, isUnscopedDiffRevisionSpec, runPortableTextDiff, DIFF_OUTPUT_MAX_CHARS, safeTempExtension } = __plasticDiffInternals;
11
-
12
- assert.deepEqual(
13
- resolveDiffFileRevision("Assets/Scene.unity"),
14
- { supplied: null, kind: "workspace-base", resolved: "Assets/Scene.unity" },
15
- "omitting revision should select the workspace-base path",
16
- );
17
- assert.deepEqual(
18
- resolveDiffFileRevision("Assets/Scene.unity", "42"),
19
- { supplied: "42", kind: "changeset", resolved: "Assets/Scene.unity#cs:42" },
20
- "a number must be a changeset selector, not an item revision",
21
- );
22
- assert.equal(resolveDiffFileRevision("Assets/Scene.unity", "br:/dev/diff").kind, "branch");
23
- assert.equal(resolveDiffFileRevision("Assets/Scene.unity", "revid:123").kind, "global-revision");
24
- assert.throws(() => resolveDiffFileRevision("Assets/Scene.unity", "base"), /Unsupported revision 'base'/);
25
- assert.throws(() => resolveDiffFileRevision("Assets/Scene.unity", "cs:head"), /Unsupported revision 'cs:head'/);
26
- assert.throws(() => resolveDiffFileRevision("Assets/Scene.unity", "cs:br:\/main"), /Unsupported revision/);
27
- assert.equal(isUnscopedDiffRevisionSpec("base"), true);
28
- assert.equal(isUnscopedDiffRevisionSpec("head"), true);
29
- assert.equal(isUnscopedDiffRevisionSpec("cs:42"), true);
30
- assert.equal(isUnscopedDiffRevisionSpec("42"), true);
31
- assert.equal(isUnscopedDiffRevisionSpec("rev:"), true);
32
- assert.equal(isUnscopedDiffRevisionSpec("arbitrary"), true);
33
- assert.equal(isUnscopedDiffRevisionSpec("Assets/Scene.unity#cs:42"), false);
34
- assert.equal(isUnscopedDiffRevisionSpec("Assets/Scene.unity#br:/main"), false);
35
- assert.equal(isUnscopedDiffRevisionSpec("revid:123"), false);
36
- assert.equal(safeTempExtension("Assets/My Scene.unity"), ".unity");
37
- assert.equal(safeTempExtension("revid:99"), ".tmp");
38
- assert.equal(__plasticProcessInternals.resolveDiffExecutable({ PI_PLASTIC_DIFF_EXECUTABLE: "D:/tools/diff.exe" }), "D:/tools/diff.exe");
39
-
40
- const left = join(root, "Unity Scene ünicode.unity");
41
- const right = join(root, "Unity Scene ünicode copy.unity");
42
- await writeFile(left, "%YAML 1.1\n--- !u!1 &1\nGameObject:\n m_Name: Before\n", "utf8");
43
- await writeFile(right, "%YAML 1.1\n--- !u!1 &1\nGameObject:\n m_Name: After\n", "utf8");
44
- const yamlResult = await runPortableTextDiff(left, right, root, "Assets/Scene.unity (Plastic base)", "Assets/Scene.unity (workspace)");
45
- assert.equal(yamlResult.backend, "diff");
46
- assert.equal(yamlResult.changed, true);
47
- assert.equal(yamlResult.binary, false, "Unity YAML must be treated as text based on bytes, not extension/attributes");
48
- assert.match(yamlResult.output, /^--- Assets\/Scene\.unity \(Plastic base\)\n\+\+\+ Assets\/Scene\.unity \(workspace\)/);
49
- assert.doesNotMatch(yamlResult.output, new RegExp(root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), "temporary/local paths must not appear in normalized headers");
50
-
51
- const sameResult = await runPortableTextDiff(left, left, root, "left", "right");
52
- assert.equal(sameResult.changed, false);
53
- assert.equal(sameResult.binary, false);
54
-
55
- const binaryLeft = join(root, "fixture.asset");
56
- const binaryRight = join(root, "fixture-copy.asset");
57
- await writeFile(binaryLeft, Buffer.from([0, 1, 2, 3]));
58
- await writeFile(binaryRight, Buffer.from([0, 1, 4, 3]));
59
- const binaryResult = await runPortableTextDiff(binaryLeft, binaryRight, root, "base", "workspace");
60
- assert.equal(binaryResult.changed, true);
61
- assert.equal(binaryResult.binary, true);
62
- assert.equal(binaryResult.output, "");
63
-
64
- const invalidUtf8Left = join(root, "invalid-utf8-left.asset");
65
- const invalidUtf8Right = join(root, "invalid-utf8-right.asset");
66
- await writeFile(invalidUtf8Left, Buffer.from([0xff, 0x81, 0x41]));
67
- await writeFile(invalidUtf8Right, Buffer.from([0xff, 0x81, 0x42]));
68
- const invalidUtf8Result = await runPortableTextDiff(invalidUtf8Left, invalidUtf8Right, root, "base", "workspace");
69
- assert.equal(invalidUtf8Result.changed, true);
70
- assert.equal(invalidUtf8Result.binary, true, "non-UTF-8 bytes must be reported as binary instead of replacement-decoded text");
71
- assert.equal(invalidUtf8Result.output, "");
72
-
73
- const largeLeft = join(root, "large-left.txt");
74
- const largeRight = join(root, "large-right.txt");
75
- await writeFile(largeLeft, `${"before\n".repeat(12_000)}`, "utf8");
76
- await writeFile(largeRight, `${"after\n".repeat(12_000)}`, "utf8");
77
- const largeResult = await runPortableTextDiff(largeLeft, largeRight, root, "large base", "large workspace");
78
- assert.equal(largeResult.changed, true);
79
- assert.equal(largeResult.truncated, true);
80
- assert.ok(largeResult.totalChars > DIFF_OUTPUT_MAX_CHARS);
81
- assert.match(largeResult.output, /Diff output truncated/);
82
-
83
- console.log("PASS: plastic diff workflow tests passed");
84
- } finally {
85
- await rm(root, { recursive: true, force: true });
86
- }
@@ -1,224 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { dirname } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { PiToolHarness, type RegisteredTool, type ToolSourceInfo } from "./pi-tool-harness.ts";
5
- import {
6
- BALANCED_ACTIVE_PLASTIC_TOOL_NAMES,
7
- PLASTIC_SEARCH_CATALOG,
8
- PLASTIC_TOOL_LOADING_MODE_ENV,
9
- PLASTIC_TOOL_NAMES,
10
- PLASTIC_TOOL_SEARCH_NAME,
11
- searchPlasticTools,
12
- } from "../src/plastic-tool-loading.ts";
13
-
14
- const publicToolNames = [...PLASTIC_TOOL_NAMES];
15
- const foreignSource: ToolSourceInfo = { path: "/extensions/foreign.ts", source: "extension", scope: "project", origin: "package" };
16
-
17
- async function withMode<T>(mode: string | undefined, action: () => Promise<T>): Promise<T> {
18
- const previous = process.env[PLASTIC_TOOL_LOADING_MODE_ENV];
19
- if (mode === undefined) delete process.env[PLASTIC_TOOL_LOADING_MODE_ENV];
20
- else process.env[PLASTIC_TOOL_LOADING_MODE_ENV] = mode;
21
- try {
22
- return await action();
23
- } finally {
24
- if (previous === undefined) delete process.env[PLASTIC_TOOL_LOADING_MODE_ENV];
25
- else process.env[PLASTIC_TOOL_LOADING_MODE_ENV] = previous;
26
- }
27
- }
28
-
29
- async function loadHarness(activeTools: string[], branchEntries: unknown[] = [], options: ConstructorParameters<typeof PiToolHarness>[0] = {}): Promise<PiToolHarness> {
30
- const harness = new PiToolHarness({ activeTools, branchEntries, ...options });
31
- await harness.load();
32
- await harness.startSession();
33
- return harness;
34
- }
35
-
36
- async function main(): Promise<void> {
37
- await withMode(undefined, async () => {
38
- const harness = await loadHarness(["read", "foreign_tool", ...publicToolNames]);
39
- assert.equal(harness.registry.size, 29, "all 28 public Plastic tools plus the loader should be registered");
40
- assert(harness.registry.has(PLASTIC_TOOL_SEARCH_NAME));
41
- assert.deepEqual(new Set(harness.getActiveTools()), new Set(["read", "foreign_tool", PLASTIC_TOOL_SEARCH_NAME, ...BALANCED_ACTIVE_PLASTIC_TOOL_NAMES]));
42
- });
43
-
44
- await withMode("loader-only", async () => {
45
- const harness = await loadHarness(["read", "foreign_tool", ...publicToolNames]);
46
- assert.deepEqual(new Set(harness.getActiveTools()), new Set(["read", "foreign_tool", PLASTIC_TOOL_SEARCH_NAME]));
47
- });
48
-
49
- await withMode("all-active", async () => {
50
- const harness = await loadHarness(["read", "foreign_tool", ...publicToolNames]);
51
- assert.deepEqual(new Set(harness.getActiveTools()), new Set(["read", "foreign_tool", ...publicToolNames]), "all-active should expose all 28 current Plastic tools without the new loader");
52
- });
53
-
54
- await withMode("balanced", async () => {
55
- const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url));
56
- const relativeSource: ToolSourceInfo = {
57
- path: "./index.ts",
58
- source: "local",
59
- scope: "project",
60
- origin: "cli",
61
- baseDir: dirname(extensionPath),
62
- };
63
- const harness = await loadHarness(["read", ...publicToolNames], [], { extensionSourceInfo: relativeSource });
64
- assert.deepEqual(new Set(harness.getActiveTools()), new Set(["read", PLASTIC_TOOL_SEARCH_NAME, ...BALANCED_ACTIVE_PLASTIC_TOOL_NAMES]), "relative configured extension paths should resolve to the package-owned source");
65
- const result = await harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)!.execute("loader", { toolNames: ["plastic_branchList"] });
66
- assert.deepEqual(result.details.added, ["plastic_branchList"], "relative-path provenance should still allow deferred activation");
67
- });
68
-
69
- assert.deepEqual(searchPlasticTools({ toolNames: ["plastic_branchDelete"] }).map((match) => match.name), ["plastic_branchDelete"], "exact public names should select one known tool");
70
- assert(searchPlasticTools({ query: "shelveset" }).every((match) => match.name.includes("shelveset")), "keyword search should find the requested capability");
71
- assert.equal(searchPlasticTools({ query: "branch", limit: 99 }).length, 4, "search results should have a hard upper bound");
72
- assert.deepEqual(searchPlasticTools({ query: "branch", limit: 3 }).map((match) => match.name), ["plastic_branchList", "plastic_branchExists", "plastic_currentBranch"], "ambiguous branch searches should prioritize inspection and query tools when multiple results are requested");
73
- assert.deepEqual(searchPlasticTools({ query: "list available branches" }).map((match) => match.name), ["plastic_branchList"], "multi-term branch discovery should not activate unrelated list tools");
74
- assert.deepEqual(searchPlasticTools({ query: "list branches in a specified workspace read-only", limit: 1 }).map((match) => match.name), ["plastic_branchList"], "the first capability domain should outrank incidental workspace context");
75
- assert.deepEqual(searchPlasticTools({ query: "find existing code reviews" }).map((match) => match.name), ["plastic_codeReviewFind"], "contextual review discovery should select the smallest sufficient query tool");
76
- assert.deepEqual(searchPlasticTools({ query: "what changed" }).map((match) => match.name), ["plastic_status"], "changed-file questions should route to status rather than GUI-capable diff commands");
77
- const diffMatches = searchPlasticTools({ query: "diff" }).map((match) => match.name);
78
- assert(!diffMatches.includes("plastic_diff"), "the disabled compatibility diff alias must never be executable through search");
79
- assert.deepEqual(diffMatches.slice(0, 2), ["plastic_diffFile", "plastic_diffRevisions"], "generic diff searches should prefer the common workspace-file route before advanced revision comparison");
80
- assert.deepEqual(searchPlasticTools({ query: "workspace diff" }).map((match) => match.name), ["plastic_diffFile"], "workspace diff intent should select the file diff tool");
81
- assert.deepEqual(searchPlasticTools({ query: "compare revisions" }).map((match) => match.name), ["plastic_diffRevisions"], "explicit revision comparison should select the advanced tool");
82
- assert.deepEqual(searchPlasticTools({ toolNames: ["plastic_diff"] }).map((match) => match.name), [], "the disabled diff alias must not be exactly selectable");
83
- assert.deepEqual(searchPlasticTools({ toolNames: ["plastic_workspaceCreate"] }).map((match) => match.name), [], "workspace creation must not be discoverable until paired cleanup is available");
84
- assert.deepEqual(searchPlasticTools({ toolNames: ["foreign_tool"] }).map((match) => match.name), [], "unknown names must not be selectable");
85
- assert.equal(searchPlasticTools({ toolNames: ["plastic_branchDelete", "plastic_branchCreate", "plastic_branchList", "plastic_currentBranch"] }).length, 4, "up to four exact names should all be selected when no limit is supplied");
86
-
87
- await withMode("loader-only", async () => {
88
- const harness = await loadHarness(["read", "foreign_tool"]);
89
- const loader = harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)!;
90
-
91
- const browseCallsBefore = harness.setActiveToolsCalls.length;
92
- for (const query of [undefined, "Unity Version Control", "version control", "UVCS", "VCS"]) {
93
- const browse = await loader.execute("loader", query === undefined ? {} : { query });
94
- assert.equal(browse.details.browse, true, `${String(query)} should provide a browse response`);
95
- assert.match(browse.content[0].text, /Browse Plastic capabilities/, "browse output should give concise recovery categories");
96
- }
97
- assert.equal(harness.setActiveToolsCalls.length, browseCallsBefore, "browse and recovery responses must not activate tools");
98
-
99
- const setCallsBefore = harness.setActiveToolsCalls.length;
100
- const result = await loader.execute("loader", { toolNames: ["plastic_branchDelete", "plastic_branchCreate", "plastic_branchList", "plastic_currentBranch"] });
101
- assert.equal(result.details.added.length, 4, "all requested exact tools should activate by default");
102
- assert(harness.getActiveTools().includes("read") && harness.getActiveTools().includes("foreign_tool"), "activation must preserve foreign tools");
103
- assert.equal(harness.setActiveToolsCalls.length, setCallsBefore + 1, "new matches should be activated once");
104
- assert.match(result.content[0].text, /Confirm the full branch path/, "loader results should return selected safety guidance");
105
-
106
- const repeatCallsBefore = harness.setActiveToolsCalls.length;
107
- const repeat = await loader.execute("loader", { toolNames: ["plastic_branchDelete"] });
108
- assert.deepEqual(repeat.details.alreadyActive, ["plastic_branchDelete"]);
109
- assert.equal(harness.setActiveToolsCalls.length, repeatCallsBefore, "already-active matches must not replace the active set");
110
-
111
- const noMatchCallsBefore = harness.setActiveToolsCalls.length;
112
- const noMatch = await loader.execute("loader", { toolNames: ["unknown_tool"] });
113
- assert.deepEqual(noMatch.details.matches, []);
114
- assert.deepEqual(noMatch.details.unknownToolNames, ["unknown_tool"]);
115
- assert.match(noMatch.content[0].text, /unknown_tool/, "unknown exact requests must be reported clearly");
116
- assert.equal(harness.setActiveToolsCalls.length, noMatchCallsBefore, "no-match searches must not change active tools");
117
-
118
- const disabledDiff = await loader.execute("loader", { toolNames: ["plastic_diff"] });
119
- assert.deepEqual(disabledDiff.details.matches, []);
120
- assert.deepEqual(disabledDiff.details.unavailableToolNames, ["plastic_diff"], "the compatibility diff alias must be reported as unavailable rather than activated");
121
- });
122
-
123
- await withMode("loader-only", async () => {
124
- const activeBranch = [
125
- { type: "message", message: { role: "toolResult", addedToolNames: ["plastic_diff", "plastic_merge", "plastic_branchList", "foreign_tool", "removed_tool"] } },
126
- ];
127
- const harness = await loadHarness(["read", "foreign_tool", ...publicToolNames], activeBranch);
128
- assert(harness.getActiveTools().includes("plastic_diff"), "the disabled compatibility diff alias should remain restorable from active history");
129
- assert(harness.getActiveTools().includes("plastic_merge") && harness.getActiveTools().includes("plastic_branchList"), "valid active-branch loader additions should be restored");
130
- assert(!harness.getActiveTools().includes("removed_tool"), "unknown historical names must not be restored");
131
- assert(harness.getActiveTools().includes("foreign_tool"), "foreign active tools must survive restoration");
132
- });
133
-
134
- await withMode("loader-only", async () => {
135
- const foreignBranchCreate: RegisteredTool = {
136
- name: "plastic_branchCreate",
137
- label: "Foreign branch create",
138
- description: "Foreign collision",
139
- sourceInfo: foreignSource,
140
- execute: () => ({}),
141
- };
142
- const harness = await loadHarness(["read", "plastic_branchCreate"], [], { sourceInfoAvailable: true, foreignTools: [foreignBranchCreate] });
143
- assert(harness.getActiveTools().includes("plastic_branchCreate"), "sourceInfo ownership must preserve a name-colliding foreign active tool");
144
-
145
- const loader = harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)!;
146
- const callsBefore = harness.setActiveToolsCalls.length;
147
- const collision = await loader.execute("loader", { toolNames: ["plastic_branchCreate", "plastic_branchList"] });
148
- assert.deepEqual(collision.details.added, ["plastic_branchList"], "only the same-source effective tool may be activated");
149
- assert.deepEqual(collision.details.unavailableToolNames, ["plastic_branchCreate"], "foreign collisions should be reported as unavailable from this extension");
150
- assert.equal(harness.setActiveToolsCalls.length, callsBefore + 1);
151
-
152
- const restoredHarness = await loadHarness(["read"], [
153
- { type: "message", message: { role: "toolResult", addedToolNames: ["plastic_branchCreate", "plastic_branchList"] } },
154
- ], { sourceInfoAvailable: true, foreignTools: [foreignBranchCreate] });
155
- assert(!restoredHarness.getActiveTools().includes("plastic_branchCreate"), "foreign same-name tools must not be restored from this loader's history");
156
- assert(restoredHarness.getActiveTools().includes("plastic_branchList"), "same-source historical loader additions should still restore");
157
- });
158
-
159
- await withMode("loader-only", async () => {
160
- const compatibilityHarness = await loadHarness(["read", ...publicToolNames], [], { sourceInfoAvailable: false });
161
- assert.deepEqual(
162
- compatibilityHarness.getActiveTools(),
163
- ["read", ...publicToolNames],
164
- "without sourceInfo, loader-only mode must preserve the active legacy set exactly",
165
- );
166
- assert.equal(compatibilityHarness.setActiveToolsCalls.length, 0, "unproven loader ownership must not rewrite the active set");
167
-
168
- const foreignBranchCreate: RegisteredTool = {
169
- name: "plastic_branchCreate",
170
- label: "Foreign branch create without provenance",
171
- description: "Foreign collision",
172
- sourceInfo: foreignSource,
173
- execute: () => ({}),
174
- };
175
- const initiallyActive = ["read", "foreign_tool", PLASTIC_TOOL_SEARCH_NAME, "plastic_status", "plastic_branchCreate"];
176
- const harness = await loadHarness(initiallyActive, [], { sourceInfoAvailable: false, foreignTools: [foreignBranchCreate] });
177
- assert.deepEqual(harness.getActiveTools(), initiallyActive, "without sourceInfo, startup must preserve every active Plastic-named tool");
178
- assert.equal(harness.setActiveToolsCalls.length, 0, "without sourceInfo, startup must not defer or otherwise rewrite the active set when the loader is already active");
179
-
180
- const loader = harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)!;
181
- const collision = await loader.execute("loader", { toolNames: ["plastic_branchCreate", "plastic_branchList"] });
182
- assert.deepEqual(collision.details.matches, ["plastic_branchCreate"], "without provenance, the loader may report only already-active known matches");
183
- assert.deepEqual(collision.details.added, [], "without provenance, the loader must not activate inactive Plastic names");
184
- assert.deepEqual(collision.details.unavailableToolNames, ["plastic_branchList"]);
185
- assert.deepEqual(harness.getActiveTools(), initiallyActive, "a foreign colliding plastic_* tool must not cause deactivation or activation without sourceInfo");
186
- assert.equal(harness.setActiveToolsCalls.length, 0, "a no-sourceInfo loader request must not change the active set");
187
- });
188
-
189
- for (const mode of ["loader-only", "all-active"] as const) await withMode(mode, async () => {
190
- const foreignLoader: RegisteredTool = {
191
- name: PLASTIC_TOOL_SEARCH_NAME,
192
- label: "Foreign Plastic loader",
193
- description: "Foreign collision",
194
- sourceInfo: foreignSource,
195
- execute: () => ({}),
196
- };
197
- const foreignBranchList: RegisteredTool = {
198
- name: "plastic_branchList",
199
- label: "Foreign branch list",
200
- description: "Foreign collision from the same source as the foreign loader",
201
- sourceInfo: foreignSource,
202
- execute: () => ({}),
203
- };
204
- const active = ["read", PLASTIC_TOOL_SEARCH_NAME, "plastic_branchList", "plastic_status"];
205
- const harness = await loadHarness(active, [], { sourceInfoAvailable: true, foreignTools: [foreignLoader, foreignBranchList] });
206
- assert.deepEqual(harness.getActiveTools(), active, `${mode} must preserve an active foreign loader and same-source Plastic collision`);
207
- assert.equal(harness.setActiveToolsCalls.length, 0, `${mode} must not rewrite tools when this extension does not own the effective loader`);
208
- });
209
-
210
- await withMode("loader-only", async () => {
211
- const harness = await loadHarness(["read"]);
212
- for (const name of PLASTIC_SEARCH_CATALOG.map((entry) => entry.name)) {
213
- const tool = harness.registry.get(name)!;
214
- assert.equal(tool.promptSnippet, undefined, `${name} should not rebuild the prompt after deferred activation`);
215
- assert.equal(tool.promptGuidelines, undefined, `${name} should not add deferred prompt guidance`);
216
- }
217
- assert(harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)?.promptSnippet, "the always-active loader should explain discovery");
218
- assert(harness.registry.get(PLASTIC_TOOL_SEARCH_NAME)?.promptGuidelines?.length, "the always-active loader should retain universal safety guidance");
219
- });
220
-
221
- console.log("PASS: plastic dynamic tool loading test succeeded");
222
- }
223
-
224
- void main();
@@ -1,34 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
-
4
- function main(): void {
5
- const indexText = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
6
- const packageManifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
7
- const branchStatusText = readFileSync(new URL("../extensions/plastic-branch-status.ts", import.meta.url), "utf8");
8
-
9
- assert.match(indexText, /parameters:\s*buildParameters\(coreTool\.args\)/, "plastic tools should derive schemas from core args without approval-only parameters");
10
- assert.match(indexText, /prepareArguments:\s*config\.prepareArguments/, "plastic tools should wire prepareArguments");
11
- assert.match(indexText, /core\.runWithAbortSignal\(signal, async \(\) => coreTool\.execute\(normalizedParams\)\)/, "plastic tools should propagate abort signals into core execution");
12
- assert.doesNotMatch(indexText, /authorizationToken|authorizationProvenance|ctx\.ui\.confirm/, "Plastic tool registration must not implement token or UI-confirmation approvals");
13
-
14
- assert.match(indexText, /assignAlias\(input, "message", \["comment", "comments"\]\);/, "plastic_checkin should normalize comment aliases");
15
- assert.match(indexText, /assignAlias\(input, "pendingChanges", \["pending_changes"\]\);/, "plastic_switchBranch should normalize pending_changes");
16
- assert.match(indexText, /"patch",/, "plastic_patch should be included in registered exports");
17
- assert.match(indexText, /"mergeToBranch",/, "plastic_mergeToBranch should be included in registered exports");
18
- assert.doesNotMatch(indexText, /"workspaceCreate",/, "plastic_workspaceCreate should remain unregistered until paired cleanup is available");
19
- assert.match(indexText, /assignAlias\(input, "toolPath", \["tool_path", "tool"\]\);/, "plastic_patch should normalize toolPath aliases");
20
- assert.match(indexText, /assignAlias\(input, "output", \["output_file", "outputFile"\]\);/, "plastic_patch should normalize output aliases");
21
- assert.match(indexText, /assignAlias\(input, "titleLike", \["title_like"\]\);/, "plastic_codeReviewFind should normalize title_like");
22
- assert.match(indexText, /assignAlias\(input, "keepOnDisk", \["keep_on_disk", "keepOnDisk", "nodisk"\]\);/, "plastic_resolveDeleteChangeConflict should normalize keepOnDisk aliases");
23
- assert.match(indexText, /assignAlias\(input, "source", \["sourceBranch", "source_branch", "branch"\]\);/, "plastic_mergeToBranch should normalize source branch aliases");
24
- assert.match(indexText, /assignAlias\(input, "cardRef", \["card", "cardCode", "card_code", "codecksCard", "codecks_card"\]\);/, "plastic_mergeToBranch should normalize card aliases");
25
- assert.match(indexText, /assignAlias\(input, "source", \["mergeSource", "merge_source"\]\);/, "plastic_finalizeMerge should normalize merge source aliases");
26
- assert.match(indexText, /assignAlias\(input, "workdir", \["cwd", "workingDirectory", "working_directory"\]\);/, "plastic tools should normalize workdir aliases");
27
- assert.match(indexText, /const enumSchema =/, "plastic tools should expose explicit enum schemas");
28
- assert.ok(packageManifest.pi.extensions.includes("./extensions/plastic-branch-status.ts"), "package should register the Plastic branch status extension");
29
- assert.doesNotMatch(branchStatusText, /\.setFooter\s*\(/, "Plastic branch status must compose through setStatus rather than replace the footer");
30
-
31
- console.log("PASS: plastic extension registration test succeeded");
32
- }
33
-
34
- main();
@@ -1,37 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { resolve } from "node:path";
3
- import { invokeRegisteredTool } from "./pi-tool-harness.ts";
4
-
5
- type ToolEnvelope = {
6
- ok?: boolean;
7
- data?: Record<string, unknown>;
8
- };
9
-
10
- const parseToolEnvelope = (text: string): ToolEnvelope => {
11
- const fencedJson = text.match(/```json\s*([\s\S]*?)\s*```/);
12
- assert(fencedJson, "Expected a structured JSON tool result.");
13
- return JSON.parse(fencedJson[1]) as ToolEnvelope;
14
- };
15
-
16
- const configuredWorkspace = process.env.PI_PLASTIC_TEST_WORKSPACE?.trim();
17
-
18
- if (!configuredWorkspace) {
19
- console.log("SKIP: set PI_PLASTIC_TEST_WORKSPACE to run opt-in Plastic live smoke tests");
20
- process.exit(0);
21
- }
22
-
23
- const workspace = resolve(configuredWorkspace);
24
- const status = parseToolEnvelope(await invokeRegisteredTool("plastic_status", { short: true, format: "json" }, workspace));
25
- assert.equal(status.ok, true, "Expected Plastic status to succeed.");
26
- const summary = status.data?.summary as Record<string, unknown> | undefined;
27
- const mergeState = status.data?.mergeState as Record<string, unknown> | undefined;
28
- assert.equal(summary?.totalPending, 0, "Live smoke tests require a clean workspace.");
29
- assert.equal(mergeState?.hasMergeInProgress, false, "Live smoke tests require no merge in progress.");
30
-
31
- const currentBranch = parseToolEnvelope(await invokeRegisteredTool("plastic_currentBranch", { format: "json" }, workspace));
32
- assert.equal(currentBranch.data?.branch, "/main", "Live sandbox must be on /main.");
33
-
34
- const mainExists = await invokeRegisteredTool("plastic_branchExists", { branch: "/main" }, workspace);
35
- assert.equal(mainExists.trim(), "true", "Expected plastic_branchExists to find the full /main path.");
36
-
37
- console.log("PASS: read-only Plastic live smoke tests passed for the configured sandbox");
@@ -1,61 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { EventEmitter } from "node:events";
3
- import { Readable } from "node:stream";
4
- import { runWithAbortSignal, update } from "../src/plastic-core.ts";
5
- import { loadRegisteredTools } from "./pi-tool-harness.ts";
6
-
7
- const registeredTools = await loadRegisteredTools();
8
- for (const [name, registeredTool] of registeredTools) {
9
- if (!name.startsWith("plastic_") || name === "plastic_tool_search") continue;
10
- assert.equal(
11
- (registeredTool.parameters as any)?.properties?.authorizationToken,
12
- undefined,
13
- `${name} must not expose mutation authorization tokens`,
14
- );
15
- }
16
-
17
- type SpawnCall = {
18
- command: string;
19
- args: string[];
20
- cwd: string;
21
- };
22
-
23
- function makeSpawn(calls: SpawnCall[], exitCode: number, stderr = "") {
24
- return ((command: string, args: readonly string[], options: { cwd: string }) => {
25
- calls.push({ command, args: [...args], cwd: options.cwd });
26
- const child = new EventEmitter() as any;
27
- child.stdout = Readable.from(exitCode === 0 ? ["Updated successfully"] : []);
28
- child.stderr = Readable.from(stderr ? [stderr] : []);
29
- child.stdin = undefined;
30
- child.kill = () => true;
31
- process.nextTick(() => child.emit("close", exitCode));
32
- return child;
33
- }) as any;
34
- }
35
-
36
- const workdir = process.cwd();
37
- const successfulCalls: SpawnCall[] = [];
38
- const output = await runWithAbortSignal(
39
- undefined,
40
- () => update.execute({ workdir }),
41
- { spawn: makeSpawn(successfulCalls, 0) },
42
- );
43
- assert.equal(output, "Updated successfully");
44
- assert.deepEqual(successfulCalls, [{
45
- command: process.env.PI_PLASTIC_CM_EXECUTABLE?.trim() || "cm",
46
- args: ["update", "--dontmerge", "--noinput"],
47
- cwd: workdir,
48
- }], "a directly invoked mutation tool must make one exact cm spawn without approval context");
49
-
50
- const failedCalls: SpawnCall[] = [];
51
- await assert.rejects(
52
- runWithAbortSignal(
53
- undefined,
54
- () => update.execute({ workdir }),
55
- { spawn: makeSpawn(failedCalls, 1, "fixture mutation failure") },
56
- ),
57
- /fixture mutation failure/,
58
- );
59
- assert.equal(failedCalls.length, 1, "a failed mutation process must not be retried implicitly");
60
-
61
- console.log("PASS: Plastic direct mutation execution tests passed");
@@ -1,118 +0,0 @@
1
- import { __plasticPatchInternals } 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 assertArgs = (actual: string[], expected: string[], message: string): void =>
12
- {
13
- assert(JSON.stringify(actual) === JSON.stringify(expected), `${message}. Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}.`);
14
- };
15
-
16
- const assertThrows = (fn: () => void, expectedMessage: string, message: string): void =>
17
- {
18
- try
19
- {
20
- fn();
21
- }
22
- catch (error)
23
- {
24
- assert(error instanceof Error, `${message}: expected an Error instance.`);
25
- assert(error.message.includes(expectedMessage), `${message}: expected message to include '${expectedMessage}', got '${error.message}'.`);
26
- return;
27
- }
28
-
29
- throw new Error(`${message}: expected function to throw.`);
30
- };
31
-
32
- const main = (): void =>
33
- {
34
- const { buildPatchCommandArgs, resolvePatchToolPath } = __plasticPatchInternals;
35
-
36
- assert(resolvePatchToolPath() === "diff", "Expected patch backend to share the portable diff PATH default.");
37
- assert(resolvePatchToolPath(" C:\\gnu\\diff.exe ") === "C:\\gnu\\diff.exe", "Expected an explicit patch backend to override the default.");
38
-
39
- assertArgs(
40
- buildPatchCommandArgs({ source: "br:/main/task001" }),
41
- ["patch", "br:/main/task001"],
42
- "Expected one-spec patch command",
43
- );
44
-
45
- assertArgs(
46
- buildPatchCommandArgs({ source: "br:/main/task001", clean: true }),
47
- ["patch", "br:/main/task001", "--clean"],
48
- "Expected clean flag to be included",
49
- );
50
-
51
- assertArgs(
52
- buildPatchCommandArgs({ source: "br:/main/task001", integration: true }),
53
- ["patch", "br:/main/task001", "--integration"],
54
- "Expected integration flag to be included",
55
- );
56
-
57
- assertArgs(
58
- buildPatchCommandArgs({ source: "br:/main/task001", clean: true, integration: true }),
59
- ["patch", "br:/main/task001", "--clean", "--integration"],
60
- "Expected clean and integration flags in stable order",
61
- );
62
-
63
- assertArgs(
64
- buildPatchCommandArgs({ source: "cs:2", destination: "cs:4", output: "review.patch" }),
65
- ["patch", "cs:2", "cs:4", "--output=review.patch"],
66
- "Expected destination before output flag",
67
- );
68
-
69
- assertArgs(
70
- buildPatchCommandArgs({ source: "br:/main/task001", toolPath: "C:\\gnu\\diff.exe" }),
71
- ["patch", "br:/main/task001", "--tool=C:\\gnu\\diff.exe"],
72
- "Expected custom diff tool path flag",
73
- );
74
-
75
- assertArgs(
76
- buildPatchCommandArgs({ source: "br:/main/task001", clean: false, integration: false }),
77
- ["patch", "br:/main/task001"],
78
- "Expected false booleans to be omitted",
79
- );
80
-
81
- assertThrows(
82
- () => buildPatchCommandArgs({ source: "" }),
83
- "source must be non-empty",
84
- "Expected blank source to be rejected",
85
- );
86
-
87
- assertThrows(
88
- () => buildPatchCommandArgs({ source: "br:/main/task001", destination: " " }),
89
- "destination must be non-empty",
90
- "Expected blank destination to be rejected",
91
- );
92
-
93
- assertThrows(
94
- () => buildPatchCommandArgs({ source: "br:/main/task001", output: " " }),
95
- "output must be non-empty",
96
- "Expected blank output to be rejected",
97
- );
98
-
99
- assertThrows(
100
- () => buildPatchCommandArgs({ source: "br:/main/task001", toolPath: " " }),
101
- "toolPath must be non-empty",
102
- "Expected blank toolPath to be rejected",
103
- );
104
-
105
- const allArgs = buildPatchCommandArgs({
106
- source: "br:/main/task001",
107
- destination: "br:/main",
108
- output: "review.patch",
109
- toolPath: "C:\\gnu\\diff.exe",
110
- clean: true,
111
- integration: true,
112
- });
113
- assert(!allArgs.some((arg) => arg === "--apply" || arg.startsWith("--apply=")), "Patch generation helper must not emit --apply.");
114
-
115
- console.log("PASS: plastic patch command tests succeeded");
116
- };
117
-
118
- main();