@jwilger/pi-development-system 0.3.6 → 0.3.8
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/package.json +1 -1
- package/src/core/git-intent.ts +4 -1
- package/src/core/test-paths.ts +10 -1
- package/src/core/test-weakening.ts +8 -2
- package/src/gates/test-guard.ts +33 -29
package/package.json
CHANGED
package/src/core/git-intent.ts
CHANGED
|
@@ -30,7 +30,10 @@ function splitLines(command: string): string[] {
|
|
|
30
30
|
let quote: "'" | '"' | undefined;
|
|
31
31
|
for (let i = 0; i < command.length; i++) {
|
|
32
32
|
const ch = command.charAt(i);
|
|
33
|
-
if (quote
|
|
33
|
+
if (quote === '"' && ch === "\\") {
|
|
34
|
+
current += ch + command.charAt(i + 1);
|
|
35
|
+
i++;
|
|
36
|
+
} else if (quote !== undefined) {
|
|
34
37
|
if (ch === quote) quote = undefined;
|
|
35
38
|
current += ch;
|
|
36
39
|
} else if (ch === "\\" && command.charAt(i + 1) === "\n") {
|
package/src/core/test-paths.ts
CHANGED
|
@@ -49,6 +49,15 @@ export function isTestPath(path: string, profile?: TestPathProfile): boolean {
|
|
|
49
49
|
return (profile?.testGlobs ?? []).some((g) => matchGlob(g, normalized));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** A malformed `file://` URL is treated as the raw path rather than throwing out of a guard. */
|
|
53
|
+
function decodeFileUrl(url: string): string {
|
|
54
|
+
try {
|
|
55
|
+
return fileURLToPath(url);
|
|
56
|
+
} catch {
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Repo-relative form of a path exactly as pi would resolve it: a leading `@` is dropped, `~` expands
|
|
54
63
|
* to the home directory and `file://` URLs are decoded, so guards see the file that is really touched.
|
|
@@ -57,6 +66,6 @@ export function normalizeRepoPath(cwd: string, input: string, home: string): str
|
|
|
57
66
|
let path = input.startsWith("@") ? input.slice(1) : input;
|
|
58
67
|
if (path === "~") path = home;
|
|
59
68
|
else if (path.startsWith("~/")) path = join(home, path.slice(2));
|
|
60
|
-
else if (/^file:\/\//.test(path)) path =
|
|
69
|
+
else if (/^file:\/\//.test(path)) path = decodeFileUrl(path);
|
|
61
70
|
return isAbsolute(path) ? relative(cwd, path) : relative(cwd, resolve(cwd, path)) || ".";
|
|
62
71
|
}
|
|
@@ -6,7 +6,7 @@ export type WeakeningSignals = { addsSkip: boolean; emptied: boolean; commentedO
|
|
|
6
6
|
const SKIP_MARKERS: readonly RegExp[] = [
|
|
7
7
|
/\b(?:it|test|describe|context)\.(?:skip|only)\b/g,
|
|
8
8
|
/\b(?:this|t|self|ctx)\.skip\(/g,
|
|
9
|
-
/\bskip:\s*true\b/g,
|
|
9
|
+
/\bskip:\s*(?:true\b|["'`])/g,
|
|
10
10
|
/\bskipTest\(/g,
|
|
11
11
|
/(?<![.\w])f(?:it|describe)\(/g,
|
|
12
12
|
/(?<![.\w])x(?:it|describe|test)\(/g,
|
|
@@ -130,7 +130,13 @@ function splitLines(command: string): string {
|
|
|
130
130
|
let quote: "'" | '"' | undefined;
|
|
131
131
|
for (let i = 0; i < command.length; i++) {
|
|
132
132
|
const c = command.charAt(i);
|
|
133
|
-
if (quote === undefined && c === "
|
|
133
|
+
if (quote === undefined && c === "#" && (out === "" || /[\s;&|(]$/.test(out))) {
|
|
134
|
+
// A comment runs to the end of the line; keep the newline so the next command is still seen.
|
|
135
|
+
while (i + 1 < command.length && command.charAt(i + 1) !== "\n") i++;
|
|
136
|
+
} else if (quote === '"' && c === "\\") {
|
|
137
|
+
out += c + command.charAt(i + 1);
|
|
138
|
+
i++;
|
|
139
|
+
} else if (quote === undefined && c === "\\" && command.charAt(i + 1) === "\n") {
|
|
134
140
|
out += " ";
|
|
135
141
|
i++;
|
|
136
142
|
} else if (quote === undefined && c === "\\") {
|
package/src/gates/test-guard.ts
CHANGED
|
@@ -70,6 +70,35 @@ const RISKY_ADDITION = /\/\*|<!--|=begin\b|"""|'''|\breturn\b|\bthrow\b|\bexit\b
|
|
|
70
70
|
|
|
71
71
|
const GLOB = /[*?[\]{]/;
|
|
72
72
|
|
|
73
|
+
const NO_SIGNALS: WeakeningSignals = { addsSkip: false, emptied: false, commentedOut: false };
|
|
74
|
+
|
|
75
|
+
/** Adding lines never weakens a test unless an added line itself disables something. */
|
|
76
|
+
const isHarmlessAddition = (before: string, after: string, signals: WeakeningSignals): boolean =>
|
|
77
|
+
!signals.addsSkip &&
|
|
78
|
+
!signals.commentedOut &&
|
|
79
|
+
isPureAddition(before, after) &&
|
|
80
|
+
!addedLines(before, after).some((line) => RISKY_ADDITION.test(line));
|
|
81
|
+
|
|
82
|
+
/** Jev's reading of a change, or `undefined` when it sees nothing to act on. */
|
|
83
|
+
function jevVerdict(
|
|
84
|
+
judged: { weakens: number; motive: string },
|
|
85
|
+
deterministicSignal: boolean,
|
|
86
|
+
): Verdict | undefined {
|
|
87
|
+
const weakening = judged.weakens >= WEAKENS_THRESHOLD;
|
|
88
|
+
if ((weakening || deterministicSignal) && judged.motive === "gate-gaming") {
|
|
89
|
+
return {
|
|
90
|
+
kind: "hard-stop",
|
|
91
|
+
why: "Jev reads this change as weakening a check to get a green result",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return weakening
|
|
95
|
+
? {
|
|
96
|
+
kind: "require-departure",
|
|
97
|
+
why: `Jev reads this change as weakening the tests (motive: ${judged.motive})`,
|
|
98
|
+
}
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
/**
|
|
74
103
|
* Soft gate `tests.weaken`: deleting, skipping, emptying or loosening tests needs a recorded
|
|
75
104
|
* departure; a change Jev reads as gaming a failing gate escalates to a hard stop.
|
|
@@ -106,41 +135,16 @@ export function registerTestGuard(deps: TestGuardDeps): void {
|
|
|
106
135
|
const judge = async (change: Change, ctx: ExtensionContext): Promise<Verdict> => {
|
|
107
136
|
const deleted = change.after === undefined && change.rewritten === undefined;
|
|
108
137
|
const after = change.after ?? change.rewritten ?? "";
|
|
109
|
-
const signals = deleted
|
|
110
|
-
|
|
111
|
-
: weakeningSignals(change.before, after);
|
|
112
|
-
// Adding lines never weakens a test unless the added line itself is a skip marker.
|
|
113
|
-
if (
|
|
114
|
-
!deleted &&
|
|
115
|
-
!signals.addsSkip &&
|
|
116
|
-
!signals.commentedOut &&
|
|
117
|
-
isPureAddition(change.before, after) &&
|
|
118
|
-
!addedLines(change.before, after).some((line) => RISKY_ADDITION.test(line))
|
|
119
|
-
) {
|
|
120
|
-
return { kind: "allow" };
|
|
121
|
-
}
|
|
138
|
+
const signals = deleted ? NO_SIGNALS : weakeningSignals(change.before, after);
|
|
139
|
+
if (!deleted && isHarmlessAddition(change.before, after, signals)) return { kind: "allow" };
|
|
122
140
|
const deterministic = describeSignals(deleted, signals);
|
|
123
141
|
const judged = await judgeTestChange(deps.jev(ctx), {
|
|
124
142
|
path: change.path,
|
|
125
143
|
before: change.before,
|
|
126
144
|
after: after === "" ? undefined : redactSecrets(after),
|
|
127
145
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const weakening = weakens >= WEAKENS_THRESHOLD || deterministic !== undefined;
|
|
131
|
-
if (weakening && motive === "gate-gaming") {
|
|
132
|
-
return {
|
|
133
|
-
kind: "hard-stop",
|
|
134
|
-
why: "Jev reads this change as weakening a check to get a green result",
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (weakens >= WEAKENS_THRESHOLD) {
|
|
138
|
-
return {
|
|
139
|
-
kind: "require-departure",
|
|
140
|
-
why: `Jev reads this change as weakening the tests (motive: ${motive})`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
146
|
+
const fromJev = judged.ok ? jevVerdict(judged.value, deterministic !== undefined) : undefined;
|
|
147
|
+
if (fromJev !== undefined) return fromJev;
|
|
144
148
|
return deterministic !== undefined
|
|
145
149
|
? { kind: "require-departure", why: `this change ${deterministic}` }
|
|
146
150
|
: { kind: "allow" };
|