@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.
- package/CHANGELOG.md +24 -0
- package/README.md +9 -7
- package/extensions/bash-cm-diff-guard.ts +2 -1
- package/extensions/file-discovery-filter.ts +67 -0
- package/index.ts +18 -2
- package/package.json +7 -8
- package/skills/_shared/references/conventional-commits.md +198 -198
- package/skills/using-plastic/SKILL.md +3 -3
- package/skills/using-plastic/references/changeset-operations.md +5 -5
- package/skills/using-plastic/references/code-review-creation.md +30 -30
- package/skills/using-plastic/references/integration.md +5 -3
- package/skills/using-plastic/references/quick-reference.md +2 -1
- package/skills/using-plastic/references/resources.md +7 -7
- package/skills/using-plastic/references/reviewing-changes.md +9 -7
- package/skills/using-plastic/references/shelving.md +25 -25
- package/skills/using-plastic/references/troubleshooting.md +5 -3
- package/skills/using-plastic/references/workspaces.md +33 -33
- package/src/file-discovery-filter-rendezvous.ts +104 -0
- package/src/{repository-search-provider.ts → file-discovery-filter.ts} +21 -17
- package/src/plastic-core.ts +579 -170
- package/src/plastic-tool-loading.ts +3 -3
- package/evals/tool-loading/README.md +0 -38
- package/evals/tool-loading/RESULTS.md +0 -57
- package/evals/tool-loading/baseline.json +0 -34
- package/evals/tool-loading/cases.json +0 -70
- package/evals/tool-loading/config.json +0 -19
- package/evals/tool-loading/mutation-guard.ts +0 -30
- package/evals/tool-loading/provider-capture.ts +0 -22
- package/evals/tool-loading/results/.gitignore +0 -3
- package/evals/tool-loading/run-eval.ts +0 -372
- package/extensions/repository-search-provider.ts +0 -66
- package/tests/bash-cm-diff-guard-validation.ts +0 -55
- package/tests/fixtures/plastic.selector +0 -3
- package/tests/fixtures/status-compact.txt +0 -1
- package/tests/fixtures/status-normal.txt +0 -1
- package/tests/packed-copy-owner-conflict.test.ts +0 -28
- package/tests/pi-tool-harness.ts +0 -116
- package/tests/plastic-branch-create-live.test.ts +0 -78
- package/tests/plastic-branch-query.test.ts +0 -26
- package/tests/plastic-branch-status.test.ts +0 -241
- package/tests/plastic-checkin-path-resolution.ts +0 -219
- package/tests/plastic-constrained-sampling.test.ts +0 -77
- package/tests/plastic-diff-workflow.test.ts +0 -86
- package/tests/plastic-dynamic-tool-loading.test.ts +0 -224
- package/tests/plastic-extension-registration.test.ts +0 -34
- package/tests/plastic-live-smoke.test.ts +0 -37
- package/tests/plastic-mutation-execution.test.ts +0 -61
- package/tests/plastic-patch-command.test.ts +0 -118
- package/tests/plastic-process-lifecycle.test.ts +0 -148
- package/tests/plastic-tool-rendering.test.ts +0 -16
- package/tests/plastic-tool-validation.ts +0 -153
- package/tests/plastic-workspace.test.ts +0 -62
- package/tests/repository-search-provider-registration.test.ts +0 -37
- package/tests/repository-search-provider.test.ts +0 -27
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { PassThrough } from "node:stream";
|
|
3
|
-
import { __plasticProcessInternals } from "../src/plastic-core.ts";
|
|
4
|
-
|
|
5
|
-
const assert = (condition: boolean, message: string): void =>
|
|
6
|
-
{
|
|
7
|
-
if (!condition)
|
|
8
|
-
{
|
|
9
|
-
throw new Error(message);
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
class FakeChildProcess extends EventEmitter
|
|
14
|
-
{
|
|
15
|
-
readonly stdout = new PassThrough();
|
|
16
|
-
readonly stderr = new PassThrough();
|
|
17
|
-
readonly stdin = new PassThrough();
|
|
18
|
-
readonly signals: string[] = [];
|
|
19
|
-
killed = false;
|
|
20
|
-
|
|
21
|
-
kill(signal?: NodeJS.Signals): boolean
|
|
22
|
-
{
|
|
23
|
-
this.killed = true;
|
|
24
|
-
this.signals.push(signal ?? "SIGTERM");
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
close(code = 0): void
|
|
29
|
-
{
|
|
30
|
-
this.stdout.end();
|
|
31
|
-
this.stderr.end();
|
|
32
|
-
this.emit("close", code, null);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const fakeSpawn = (proc: FakeChildProcess): typeof import("node:child_process").spawn =>
|
|
37
|
-
(() => proc as unknown as ReturnType<typeof import("node:child_process").spawn>) as typeof import("node:child_process").spawn;
|
|
38
|
-
|
|
39
|
-
const testExecutableResolutionAndDiagnostics = async (): Promise<void> =>
|
|
40
|
-
{
|
|
41
|
-
assert(__plasticProcessInternals.resolveCmExecutable({}) === "cm", "Expected cm to be the default Plastic executable.");
|
|
42
|
-
assert(__plasticProcessInternals.resolveCmExecutable({ PI_PLASTIC_CM_EXECUTABLE: " /tools/cm " }) === "/tools/cm", "Expected PI_PLASTIC_CM_EXECUTABLE to override cm.");
|
|
43
|
-
assert(__plasticProcessInternals.resolveGitExecutable({ PI_PLASTIC_GIT_EXECUTABLE: "C:/Tools/git.exe" }) === "C:/Tools/git.exe", "Expected PI_PLASTIC_GIT_EXECUTABLE to override git.");
|
|
44
|
-
|
|
45
|
-
const proc = new FakeChildProcess();
|
|
46
|
-
const missingCm = __plasticProcessInternals.spawnAndCollect("cm", ["status"], process.cwd(), undefined, undefined, {
|
|
47
|
-
spawn: fakeSpawn(proc),
|
|
48
|
-
});
|
|
49
|
-
const error = Object.assign(new Error("spawn cm ENOENT"), { code: "ENOENT" });
|
|
50
|
-
proc.emit("error", error);
|
|
51
|
-
|
|
52
|
-
await missingCm.then(
|
|
53
|
-
() => { throw new Error("Expected a missing executable to reject."); },
|
|
54
|
-
(reason: unknown) =>
|
|
55
|
-
{
|
|
56
|
-
assert(reason instanceof Error && reason.message.includes("PI_PLASTIC_CM_EXECUTABLE"), "Expected missing cm guidance to name PI_PLASTIC_CM_EXECUTABLE.");
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const gitProc = new FakeChildProcess();
|
|
61
|
-
const missingGit = __plasticProcessInternals.spawnAndCollect("git", ["--version"], process.cwd(), undefined, undefined, {
|
|
62
|
-
spawn: fakeSpawn(gitProc),
|
|
63
|
-
});
|
|
64
|
-
gitProc.emit("error", Object.assign(new Error("spawn git ENOENT"), { code: "ENOENT" }));
|
|
65
|
-
await missingGit.then(
|
|
66
|
-
() => { throw new Error("Expected a missing Git executable to reject."); },
|
|
67
|
-
(reason: unknown) =>
|
|
68
|
-
{
|
|
69
|
-
assert(reason instanceof Error && reason.message.includes("PI_PLASTIC_GIT_EXECUTABLE"), "Expected missing git guidance to name PI_PLASTIC_GIT_EXECUTABLE.");
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const testAbortEscalatesUntilTerminalSettlement = async (): Promise<void> =>
|
|
75
|
-
{
|
|
76
|
-
const proc = new FakeChildProcess();
|
|
77
|
-
const controller = new AbortController();
|
|
78
|
-
let scheduledKill: (() => void) | undefined;
|
|
79
|
-
let clearedTimerCount = 0;
|
|
80
|
-
|
|
81
|
-
const resultPromise = __plasticProcessInternals.spawnAndCollect("fake", [], process.cwd(), undefined, controller.signal, {
|
|
82
|
-
spawn: fakeSpawn(proc),
|
|
83
|
-
setTimeout: (callback) =>
|
|
84
|
-
{
|
|
85
|
-
scheduledKill = callback;
|
|
86
|
-
return {} as NodeJS.Timeout;
|
|
87
|
-
},
|
|
88
|
-
clearTimeout: () =>
|
|
89
|
-
{
|
|
90
|
-
clearedTimerCount += 1;
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
controller.abort();
|
|
95
|
-
assert(proc.killed, "Expected SIGTERM to mark the fake child as killed.");
|
|
96
|
-
assert(proc.signals.join(",") === "SIGTERM", "Expected abort to send SIGTERM first.");
|
|
97
|
-
assert(scheduledKill !== undefined, "Expected abort to schedule escalation.");
|
|
98
|
-
|
|
99
|
-
// ChildProcess.killed means only that SIGTERM was sent; a still-running child needs SIGKILL.
|
|
100
|
-
scheduledKill?.();
|
|
101
|
-
assert(proc.signals.join(",") === "SIGTERM,SIGKILL", "Expected a non-settled child to receive SIGKILL even after SIGTERM was sent.");
|
|
102
|
-
|
|
103
|
-
proc.close();
|
|
104
|
-
const result = await resultPromise;
|
|
105
|
-
assert(result.aborted, "Expected abort state to be reported after terminal close.");
|
|
106
|
-
assert(clearedTimerCount === 1, "Expected the escalation timer to be cleared during cleanup.");
|
|
107
|
-
|
|
108
|
-
controller.abort();
|
|
109
|
-
assert(proc.signals.join(",") === "SIGTERM,SIGKILL", "Expected abort listener cleanup after terminal settlement.");
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const testTerminalListenersExistBeforeStdinWrite = async (): Promise<void> =>
|
|
113
|
-
{
|
|
114
|
-
const proc = new FakeChildProcess();
|
|
115
|
-
proc.stdin.write = ((_chunk: unknown, _callback?: (error?: Error | null) => void): boolean =>
|
|
116
|
-
{
|
|
117
|
-
proc.stdout.end();
|
|
118
|
-
proc.stderr.end();
|
|
119
|
-
// Deliberately do not invoke the write callback: terminal error must still settle.
|
|
120
|
-
proc.emit("error", new Error("stdin-triggered process failure"));
|
|
121
|
-
return true;
|
|
122
|
-
}) as typeof proc.stdin.write;
|
|
123
|
-
|
|
124
|
-
let thrown: unknown;
|
|
125
|
-
try
|
|
126
|
-
{
|
|
127
|
-
await __plasticProcessInternals.spawnAndCollect("fake", [], process.cwd(), "input", undefined, {
|
|
128
|
-
spawn: fakeSpawn(proc),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
catch (error)
|
|
132
|
-
{
|
|
133
|
-
thrown = error;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
assert(thrown instanceof Error && thrown.message === "stdin-triggered process failure", "Expected process error raised during stdin write to reject cleanly.");
|
|
137
|
-
assert(proc.listenerCount("error") === 0 && proc.listenerCount("close") === 0, "Expected terminal listeners to be removed after rejection.");
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const main = async (): Promise<void> =>
|
|
141
|
-
{
|
|
142
|
-
await testExecutableResolutionAndDiagnostics();
|
|
143
|
-
await testAbortEscalatesUntilTerminalSettlement();
|
|
144
|
-
await testTerminalListenersExistBeforeStdinWrite();
|
|
145
|
-
console.log("PASS: plastic process lifecycle tests passed");
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
void main();
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
const indexText = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
|
|
5
|
-
|
|
6
|
-
assert.match(indexText, /const COLLAPSED_RESULT_LINES = 12;/, "plastic tool results should have a compact collapsed preview limit");
|
|
7
|
-
assert.match(indexText, /function renderPlasticCall\(/, "plastic tools should provide custom call rendering");
|
|
8
|
-
assert.match(indexText, /function renderPlasticResult\(/, "plastic tools should provide custom result rendering");
|
|
9
|
-
assert.match(indexText, /renderCall\(args, theme\) \{/, "registered plastic tools should wire renderCall");
|
|
10
|
-
assert.match(indexText, /renderResult\(result, options, theme\) \{/, "registered plastic tools should wire renderResult");
|
|
11
|
-
assert.match(indexText, /options\?\.expanded \? lines\.length : COLLAPSED_RESULT_LINES/, "collapsed result rendering should limit visible output until expansion");
|
|
12
|
-
assert.match(indexText, /ctrl\+o to expand/, "collapsed result rendering should advertise the Ctrl+O expansion shortcut");
|
|
13
|
-
assert.match(indexText, /truncateAnsiLine/, "custom rendering should remain width-safe for long output lines");
|
|
14
|
-
assert.match(indexText, /themed\(theme, "toolOutput", line\)/, "tool output lines should use the toolOutput theme color");
|
|
15
|
-
|
|
16
|
-
console.log("PASS: plastic tool rendering test succeeded");
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
2
|
-
|
|
3
|
-
const pass = (msg: string): void => console.log(`PASS: ${msg}`);
|
|
4
|
-
const fail = (msg: string): void => console.log(`FAIL: ${msg}`);
|
|
5
|
-
|
|
6
|
-
const readText = (fileURL: URL): string => readFileSync(fileURL, "utf8");
|
|
7
|
-
|
|
8
|
-
const checkRequired = (contents: string, target: string, snippets: string[]): number => {
|
|
9
|
-
let failures = 0;
|
|
10
|
-
for (const snippet of snippets) {
|
|
11
|
-
if (contents.includes(snippet)) {
|
|
12
|
-
pass(`${target} contains: ${snippet}`);
|
|
13
|
-
} else {
|
|
14
|
-
fail(`${target} missing required snippet: ${snippet}`);
|
|
15
|
-
failures += 1;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return failures;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const checkForbidden = (contents: string, target: string, patterns: RegExp[]): number => {
|
|
22
|
-
let failures = 0;
|
|
23
|
-
for (const pattern of patterns) {
|
|
24
|
-
if (pattern.test(contents)) {
|
|
25
|
-
fail(`${target} matched forbidden pattern: ${pattern}`);
|
|
26
|
-
failures += 1;
|
|
27
|
-
} else {
|
|
28
|
-
pass(`${target} does not match: ${pattern}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return failures;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const main = (): void => {
|
|
35
|
-
let failures = 0;
|
|
36
|
-
|
|
37
|
-
const plasticPath = new URL("../src/plastic-core.ts", import.meta.url);
|
|
38
|
-
const skillPath = new URL("../skills/using-plastic/SKILL.md", import.meta.url);
|
|
39
|
-
const reviewingPath = new URL("../skills/using-plastic/references/reviewing-changes.md", import.meta.url);
|
|
40
|
-
const changesetPath = new URL("../skills/using-plastic/references/changeset-operations.md", import.meta.url);
|
|
41
|
-
const troubleshootingPath = new URL("../skills/using-plastic/references/troubleshooting.md", import.meta.url);
|
|
42
|
-
const integrationPath = new URL("../skills/using-plastic/references/integration.md", import.meta.url);
|
|
43
|
-
const bashGuardPath = new URL("../extensions/bash-cm-diff-guard.ts", import.meta.url);
|
|
44
|
-
const bashMergeGuardPath = new URL("../extensions/bash-cm-merge-guard.ts", import.meta.url);
|
|
45
|
-
|
|
46
|
-
const plasticText = readText(plasticPath);
|
|
47
|
-
failures += checkRequired(plasticText, "pi-plastic/src/plastic-core.ts", [
|
|
48
|
-
"const BLOCKED_CM_DIFF_MESSAGE",
|
|
49
|
-
"ensureCmCommandAllowed(args);",
|
|
50
|
-
'command === "diff"',
|
|
51
|
-
"export const merge = tool({",
|
|
52
|
-
"export const mergeToBranch = tool({",
|
|
53
|
-
"resolveBranchParentName",
|
|
54
|
-
"export const finalizeMerge = tool({",
|
|
55
|
-
"export const resolveDeleteChangeConflict = tool({",
|
|
56
|
-
"export const patch = tool({",
|
|
57
|
-
"__plasticPatchInternals",
|
|
58
|
-
"buildPatchCommandArgs",
|
|
59
|
-
"export const diffRevisions = tool({",
|
|
60
|
-
"plastic_diff is disabled",
|
|
61
|
-
"parseMachineReadablePendingItems",
|
|
62
|
-
"summarizePendingItems",
|
|
63
|
-
"selectPrivatePathsForAutoAdd",
|
|
64
|
-
"filterPendingItemsByScope",
|
|
65
|
-
"isNoChangesWorkspaceCheckinError",
|
|
66
|
-
"SENSITIVE_PRIVATE_PATH_PATTERNS",
|
|
67
|
-
"auto-add-private-retry-success",
|
|
68
|
-
"__plasticSwitchInternals",
|
|
69
|
-
"normalizeBranchSpecForComparison",
|
|
70
|
-
"isSwitchBringBlockedForUnattended",
|
|
71
|
-
"canSwitchDirectWithPrivateOnlyPending",
|
|
72
|
-
"direct-switch-private-only",
|
|
73
|
-
"resolveCheckinPaths",
|
|
74
|
-
"buildFallbackScopePaths",
|
|
75
|
-
"--nointeractiveresolution",
|
|
76
|
-
"--mergetype=try",
|
|
77
|
-
"--automaticresolution=all-",
|
|
78
|
-
"FILE_CONFLICT",
|
|
79
|
-
"isMergeInProgressCheckinError",
|
|
80
|
-
"buildMergeInProgressCheckinMessage",
|
|
81
|
-
"updateAfter is disabled for unattended safety",
|
|
82
|
-
'["update", "--dontmerge", "--noinput"]',
|
|
83
|
-
]);
|
|
84
|
-
failures += checkForbidden(plasticText, "pi-plastic/src/plastic-core.ts", [
|
|
85
|
-
/runCm\(\["diff"/,
|
|
86
|
-
/runCmRaw\(\["diff"/,
|
|
87
|
-
/runCm\(\["update"\]/,
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
const skillText = readText(skillPath);
|
|
91
|
-
failures += checkRequired(skillText, "pi-plastic/skills/using-plastic/SKILL.md", [
|
|
92
|
-
"Never run `cm diff` in Pi.",
|
|
93
|
-
"plastic_diffRevisions",
|
|
94
|
-
"plastic_patch",
|
|
95
|
-
"plastic_merge",
|
|
96
|
-
"plastic_mergeToBranch",
|
|
97
|
-
"source branch's Plastic parent branch",
|
|
98
|
-
]);
|
|
99
|
-
failures += checkForbidden(skillText, "pi-plastic/skills/using-plastic/SKILL.md", [
|
|
100
|
-
/^\s*-\s+plastic_diff\s*$/m,
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
const refs = [
|
|
104
|
-
["reviewing-changes.md", readText(reviewingPath)],
|
|
105
|
-
["changeset-operations.md", readText(changesetPath)],
|
|
106
|
-
["troubleshooting.md", readText(troubleshootingPath)],
|
|
107
|
-
["integration.md", readText(integrationPath)],
|
|
108
|
-
] as const;
|
|
109
|
-
|
|
110
|
-
for (const [name, text] of refs) {
|
|
111
|
-
failures += checkForbidden(text, `pi-plastic/skills/using-plastic/references/${name}`, [
|
|
112
|
-
/cm diff\s+br:/,
|
|
113
|
-
/cm diff\s+cs:/,
|
|
114
|
-
/cm diff\s+lb:/,
|
|
115
|
-
/cm diff\s+sh:/,
|
|
116
|
-
/cm diff\s+rev:/,
|
|
117
|
-
/plastic_diff\(source=/,
|
|
118
|
-
/list changed files/i,
|
|
119
|
-
]);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const bashGuardText = readText(bashGuardPath);
|
|
123
|
-
failures += checkRequired(bashGuardText, "pi-plastic/extensions/bash-cm-diff-guard.ts", [
|
|
124
|
-
'pi.on("tool_call"',
|
|
125
|
-
"isBashToolCall",
|
|
126
|
-
"commandRunsCmDiff",
|
|
127
|
-
"__bashCmDiffGuardInternals",
|
|
128
|
-
"block: true",
|
|
129
|
-
"reason: BLOCK_MESSAGE",
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
const bashMergeGuardText = readText(bashMergeGuardPath);
|
|
133
|
-
failures += checkRequired(bashMergeGuardText, "pi-plastic/extensions/bash-cm-merge-guard.ts", [
|
|
134
|
-
'pi.on("tool_call"',
|
|
135
|
-
"isBashToolCall",
|
|
136
|
-
"commandRunsUnsafeCmMerge",
|
|
137
|
-
"__bashCmMergeGuardInternals",
|
|
138
|
-
"plastic_resolveDeleteChangeConflict",
|
|
139
|
-
"--nointeractiveresolution",
|
|
140
|
-
"--mergetype=try",
|
|
141
|
-
"block: true",
|
|
142
|
-
"reason: BLOCK_MESSAGE",
|
|
143
|
-
]);
|
|
144
|
-
|
|
145
|
-
if (failures > 0) {
|
|
146
|
-
console.log(`FAIL: plastic validation failed with ${failures} issue(s)`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
console.log("PASS: plastic validation succeeded");
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
main();
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
discoverPlasticWorkspace,
|
|
7
|
-
parsePlasticSelector,
|
|
8
|
-
parsePlasticStatusBranch,
|
|
9
|
-
sanitizePlasticBranch,
|
|
10
|
-
} from "../src/plastic-workspace.ts";
|
|
11
|
-
|
|
12
|
-
const fixture = async (name: string): Promise<string> => readFile(new URL(`./fixtures/${name}`, import.meta.url), "utf8");
|
|
13
|
-
|
|
14
|
-
const root = await mkdtemp(join(tmpdir(), "pi-plastic-workspace-"));
|
|
15
|
-
try {
|
|
16
|
-
const outer = join(root, "outer");
|
|
17
|
-
const inner = join(outer, "nested", "inner");
|
|
18
|
-
const cwd = join(inner, "src", "feature");
|
|
19
|
-
await mkdir(join(outer, ".plastic"), { recursive: true });
|
|
20
|
-
await writeFile(join(outer, ".plastic", "plastic.workspace"), "sanitized workspace marker\n");
|
|
21
|
-
await mkdir(join(inner, ".plastic"), { recursive: true });
|
|
22
|
-
await writeFile(join(inner, ".plastic", "plastic.workspace"), "nested sanitized marker\n");
|
|
23
|
-
await mkdir(cwd, { recursive: true });
|
|
24
|
-
|
|
25
|
-
const nested = await discoverPlasticWorkspace(cwd);
|
|
26
|
-
assert.equal(nested.kind, "found");
|
|
27
|
-
if (nested.kind === "found") assert.equal(nested.value.root, await realpath(inner));
|
|
28
|
-
|
|
29
|
-
await rm(join(inner, ".plastic", "plastic.workspace"));
|
|
30
|
-
const outerResult = await discoverPlasticWorkspace(cwd);
|
|
31
|
-
assert.equal(outerResult.kind, "found");
|
|
32
|
-
if (outerResult.kind === "found") assert.equal(outerResult.value.root, await realpath(outer));
|
|
33
|
-
|
|
34
|
-
const gitOnly = join(root, "git-only", ".git");
|
|
35
|
-
await mkdir(gitOnly, { recursive: true });
|
|
36
|
-
assert.equal((await discoverPlasticWorkspace(join(root, "git-only"))).kind, "not_found");
|
|
37
|
-
|
|
38
|
-
const selector = parsePlasticSelector(await fixture("plastic.selector"));
|
|
39
|
-
assert.deepEqual(selector, {
|
|
40
|
-
kind: "found",
|
|
41
|
-
value: { repository: "sanitized-repository@sanitized-server", branch: "/main/feature/footer" },
|
|
42
|
-
});
|
|
43
|
-
assert.equal(parsePlasticSelector("repository sample@server\n path /\n").kind, "not_found");
|
|
44
|
-
assert.equal(parsePlasticSelector("smartbranch \n").kind, "malformed");
|
|
45
|
-
assert.equal(parsePlasticSelector("smartbranch /main/bad\u0007branch\n").kind, "malformed");
|
|
46
|
-
|
|
47
|
-
const normal = parsePlasticStatusBranch(await fixture("status-normal.txt"));
|
|
48
|
-
const compact = parsePlasticStatusBranch(await fixture("status-compact.txt"));
|
|
49
|
-
assert.deepEqual(normal, { kind: "found", value: { branch: "/main/feature/footer" } });
|
|
50
|
-
assert.deepEqual(compact, normal, "normal and compact status forms should yield the same branch path");
|
|
51
|
-
assert.deepEqual(parsePlasticStatusBranch("status cs:731\n"), { kind: "found", value: { changesetId: "731" } });
|
|
52
|
-
assert.equal(parsePlasticStatusBranch("unexpected output\n").kind, "malformed");
|
|
53
|
-
assert.equal(parsePlasticStatusBranch("\n").kind, "not_found");
|
|
54
|
-
|
|
55
|
-
assert.equal(sanitizePlasticBranch(" /main/feature\u0007 footer "), "/main/feature footer");
|
|
56
|
-
assert.equal(sanitizePlasticBranch("x".repeat(81)), `${"x".repeat(79)}…`);
|
|
57
|
-
assert.equal(sanitizePlasticBranch("界".repeat(41)), `${"界".repeat(39)}…`);
|
|
58
|
-
} finally {
|
|
59
|
-
await rm(root, { recursive: true, force: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log("PASS: Plastic workspace discovery and parser tests passed");
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { createPlasticRepositoryProviderExtension, isMissingRepoSearchRuntime } from "../extensions/repository-search-provider.ts";
|
|
3
|
-
|
|
4
|
-
const records = new WeakMap<object, Map<string, { token: { id: string; nonce: string }; policy: { id: string } }>>();
|
|
5
|
-
let nonce = 0;
|
|
6
|
-
const registry = {
|
|
7
|
-
register(scope: object, policy: { id: string }) {
|
|
8
|
-
const scoped = records.get(scope) ?? new Map();
|
|
9
|
-
if (scoped.has(policy.id)) throw new Error(`duplicate ${policy.id}`);
|
|
10
|
-
const token = { id: policy.id, nonce: String(++nonce) };
|
|
11
|
-
scoped.set(policy.id, { token, policy }); records.set(scope, scoped); return token;
|
|
12
|
-
},
|
|
13
|
-
unregister(token: { id: string; nonce: string }) {
|
|
14
|
-
for (const scoped of [records.get(scope)]) {
|
|
15
|
-
const current = scoped?.get(token.id);
|
|
16
|
-
if (current?.token.nonce === token.nonce) return scoped!.delete(token.id);
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
const runtime = { createRepositoryPolicyRegistryV1: () => registry };
|
|
22
|
-
const handlers = new Map<string, Array<(_event: unknown, ctx: any) => Promise<void> | void>>();
|
|
23
|
-
const pi = { on(event: string, handler: (_event: unknown, ctx: any) => Promise<void> | void) { handlers.set(event, [...(handlers.get(event) ?? []), handler]); } };
|
|
24
|
-
createPlasticRepositoryProviderExtension(async () => runtime as any)(pi as any);
|
|
25
|
-
const scope = {}; const ctx = { sessionManager: scope };
|
|
26
|
-
for (const handler of handlers.get("session_start") ?? []) await handler({}, ctx);
|
|
27
|
-
assert.deepEqual([...records.get(scope)!.keys()], ["plastic.ignore-files"]);
|
|
28
|
-
for (const handler of handlers.get("session_shutdown") ?? []) await handler({}, ctx);
|
|
29
|
-
assert.equal(records.get(scope)?.size ?? 0, 0);
|
|
30
|
-
|
|
31
|
-
const absentHandlers = new Map<string, Array<(_event: unknown, ctx: any) => Promise<void> | void>>();
|
|
32
|
-
createPlasticRepositoryProviderExtension(async () => { throw Object.assign(new Error("Cannot find package '@aefree/pi-repo-search'"), { code: "ERR_MODULE_NOT_FOUND" }); })({ on(event: string, handler: any) { absentHandlers.set(event, [...(absentHandlers.get(event) ?? []), handler]); } } as any);
|
|
33
|
-
for (const handler of absentHandlers.get("session_start") ?? []) await handler({}, ctx);
|
|
34
|
-
assert.equal(records.get(scope)?.size ?? 0, 0, "an absent optional package must leave pi-plastic usable without registration");
|
|
35
|
-
assert.equal(isMissingRepoSearchRuntime({ code: "ERR_MODULE_NOT_FOUND", message: "Cannot find package '@aefree/pi-repo-search'" }), true);
|
|
36
|
-
assert.equal(isMissingRepoSearchRuntime({ code: "ERR_MODULE_NOT_FOUND", message: "Cannot find package 'nested-defect'" }), false);
|
|
37
|
-
console.log("PASS: optional Plastic repository-search policy registration");
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { createPlasticRepositorySearchPolicyV1, loadPlasticOwnerV1 } from "../src/repository-search-provider.ts";
|
|
6
|
-
|
|
7
|
-
const root = await mkdtemp(join(tmpdir(), "pi-plastic-repo-policy-"));
|
|
8
|
-
try {
|
|
9
|
-
const plastic = join(root, "plastic"); const source = join(plastic, "Assets", "Code"); const gitOnly = join(root, "git-only");
|
|
10
|
-
await mkdir(join(plastic, ".plastic"), { recursive: true }); await writeFile(join(plastic, ".plastic", "plastic.workspace"), "fixture");
|
|
11
|
-
await mkdir(source, { recursive: true }); await writeFile(join(plastic, "ignore.conf"), "ignored.txt\n"); await writeFile(join(plastic, "Assets", "cloaked.conf"), "cache/**\n");
|
|
12
|
-
await mkdir(join(gitOnly, ".git"), { recursive: true });
|
|
13
|
-
const owner = await loadPlasticOwnerV1(new URL("../extensions/repository-search-provider.ts", import.meta.url).href);
|
|
14
|
-
assert.equal(owner.packageVersion, "0.4.0");
|
|
15
|
-
assert.equal(owner.packageRoot, await realpath(join(import.meta.dirname, "..")));
|
|
16
|
-
const policy = createPlasticRepositorySearchPolicyV1(owner);
|
|
17
|
-
const result = await policy.evaluate({ cwd: root, signal: new AbortController().signal }, { workspaceRoot: root, roots: [source], includeHidden: false, signal: new AbortController().signal });
|
|
18
|
-
assert.equal(result.outcome, "applied");
|
|
19
|
-
if (result.outcome === "applied") {
|
|
20
|
-
assert.equal(result.roots[0]?.policyOwnedRoot, plastic);
|
|
21
|
-
assert.deepEqual(result.roots[0]?.ignoreFiles.map((file) => file.slice(plastic.length + 1).replaceAll("\\", "/")), ["ignore.conf", "Assets/cloaked.conf"]);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const nonApplicable = await policy.evaluate({ cwd: root, signal: new AbortController().signal }, { workspaceRoot: root, roots: [gitOnly], includeHidden: false, signal: new AbortController().signal });
|
|
25
|
-
assert.equal(nonApplicable.outcome, "not_applicable");
|
|
26
|
-
} finally { await rm(root, { recursive: true, force: true }); }
|
|
27
|
-
console.log("PASS: Plastic repository-search policy");
|