test: push coverage to 90%+ on lines for all files except execute.ts (FAR-85)
Overall before: 80.36% lines / 79.06% statements Overall after: 94.65% lines / 93.30% statements Per-file lines coverage (all targets ≥90% except execute.ts): | File | Before | After | |-------------------|--------|--------| | ui-parser.ts | 93.63% | 99.09% | | cli/format-event | 59.85% | 99.27% | | server/execute | 81.47% | 89.64% | | server/job-mfst | 90.30% | 98.78% | | server/k8s-client | 37.50% | 95.83% | | server/log-dedup | 97.77% | 97.77% | | server/parse | 89.85% | 98.55% | | server/skills | 100% | 100% | New tests added: - k8s-client.test.ts: getSelfPodInfo (env-var inheritance, secret volumes, PVC discovery, dnsConfig, all error paths) + kubeconfig file branch - format-event.test.ts: parseStdoutLine (cli) — full event-type matrix, tool_use status branches, errorText fallback paths - ui-parser.test.ts: errorText edge cases, empty event paths - parse.test.ts: errorText fallback to data.message, name, code, JSON - job-manifest.test.ts: workspace context env wiring, linkedIssueIds, paperclipWorkspaces/RuntimeServices JSON, authToken, inherited URLs, prompt-secret + data PVC + secret-volume mount paths - execute.test.ts: parseModelProvider, completionWithGrace, instructionsFilePath read failure, ensureAgentDbPvc throw paths, large-prompt secret create failure, step-limit detection, waitForPod no-pod messaging, init-container ImagePullBackOff / CrashLoopBackOff, main-container CrashLoopBackOff, all-inits-done happy path, skill bundle source loading (SKILL.md + flat-file fallback), SIGTERM handler full body via vi.resetModules() execute.ts remains at 89.64% lines — the residual gap is deep async/timer paths inside streamAndAwaitJob (grace poller, keepalive ticker, log-stream stop-signal/bail timer). Those need fake-timer scaffolding heavier than this batch warrants; tracking separately. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -28,13 +28,14 @@ vi.mock("@kubernetes/client-node", () => {
|
||||
}
|
||||
}
|
||||
class KubeConfig {
|
||||
loadFromCluster() {}
|
||||
loadFromFile() {}
|
||||
loadFromCluster = mockLoadFromCluster;
|
||||
loadFromFile = mockLoadFromFile;
|
||||
makeApiClient() {
|
||||
return {
|
||||
readNamespacedPersistentVolumeClaim: mockReadNamespacedPVC,
|
||||
deleteNamespacedPersistentVolumeClaim: mockDeleteNamespacedPVC,
|
||||
createNamespacedPersistentVolumeClaim: mockCreateNamespacedPVC,
|
||||
readNamespacedPod: mockReadNamespacedPod,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -51,9 +52,17 @@ vi.mock("@kubernetes/client-node", () => {
|
||||
const mockReadNamespacedPVC = vi.fn();
|
||||
const mockDeleteNamespacedPVC = vi.fn();
|
||||
const mockCreateNamespacedPVC = vi.fn();
|
||||
const mockReadNamespacedPod = vi.fn();
|
||||
const mockLoadFromCluster = vi.fn();
|
||||
const mockLoadFromFile = vi.fn();
|
||||
const mockReadFileSync = vi.fn();
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
|
||||
}));
|
||||
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import { getPvc, createPvc, deletePvc, resetCache } from "./k8s-client.js";
|
||||
import { getPvc, createPvc, deletePvc, getSelfPodInfo, resetCache } from "./k8s-client.js";
|
||||
|
||||
const ApiException = (k8s as unknown as { ApiException: new <T>(code: number, message: string, body: T, headers?: Record<string, string>) => Error & { code: number; body: T } }).ApiException;
|
||||
|
||||
@@ -143,3 +152,145 @@ describe("createPvc — passes through to SDK", () => {
|
||||
expect(mockCreateNamespacedPVC).toHaveBeenCalledWith({ namespace: "paperclip", body: spec });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSelfPodInfo", () => {
|
||||
const HOSTNAME = "paperclip-test-pod";
|
||||
const NAMESPACE = "paperclip-test";
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.HOSTNAME = HOSTNAME;
|
||||
delete process.env.PAPERCLIP_NAMESPACE;
|
||||
delete process.env.POD_NAMESPACE;
|
||||
mockReadFileSync.mockReturnValue(NAMESPACE);
|
||||
});
|
||||
|
||||
function basePod(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: "paperclip",
|
||||
image: "paperclip:1.0",
|
||||
env: [
|
||||
{ name: "FOO", value: "bar" },
|
||||
{ name: "SECRET_REF", valueFrom: { secretKeyRef: { name: "s", key: "k" } } },
|
||||
],
|
||||
envFrom: [{ configMapRef: { name: "cm" } }],
|
||||
volumeMounts: [
|
||||
{ name: "data", mountPath: "/paperclip" },
|
||||
{ name: "tls-secret", mountPath: "/etc/tls" },
|
||||
],
|
||||
},
|
||||
],
|
||||
volumes: [
|
||||
{ name: "data", persistentVolumeClaim: { claimName: "paperclip-pvc" } },
|
||||
{ name: "tls-secret", secret: { secretName: "tls", defaultMode: 0o400 } },
|
||||
],
|
||||
imagePullSecrets: [{ name: "registry-creds" }, { name: "" }, {}],
|
||||
dnsConfig: { nameservers: ["10.0.0.10"] },
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("introspects the pod and extracts image, env, PVC, secrets, dnsConfig", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.namespace).toBe(NAMESPACE);
|
||||
expect(info.image).toBe("paperclip:1.0");
|
||||
expect(info.pvcClaimName).toBe("paperclip-pvc");
|
||||
expect(info.inheritedEnv).toEqual({ FOO: "bar" });
|
||||
expect(info.inheritedEnvValueFrom).toHaveLength(1);
|
||||
expect(info.inheritedEnvValueFrom[0].name).toBe("SECRET_REF");
|
||||
expect(info.inheritedEnvFrom).toHaveLength(1);
|
||||
expect(info.secretVolumes).toEqual([
|
||||
{ volumeName: "tls-secret", secretName: "tls", mountPath: "/etc/tls", defaultMode: 0o400 },
|
||||
]);
|
||||
// imagePullSecrets with empty name are filtered out
|
||||
expect(info.imagePullSecrets).toEqual([{ name: "registry-creds" }]);
|
||||
expect(info.dnsConfig).toEqual({ nameservers: ["10.0.0.10"] });
|
||||
expect(mockReadNamespacedPod).toHaveBeenCalledWith({ name: HOSTNAME, namespace: NAMESPACE });
|
||||
});
|
||||
|
||||
it("caches the result — second call does not re-query the API", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
await getSelfPodInfo();
|
||||
await getSelfPodInfo();
|
||||
expect(mockReadNamespacedPod).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prefers PAPERCLIP_NAMESPACE env over service-account file", async () => {
|
||||
process.env.PAPERCLIP_NAMESPACE = "from-env";
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.namespace).toBe("from-env");
|
||||
expect(mockReadFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to POD_NAMESPACE when PAPERCLIP_NAMESPACE not set", async () => {
|
||||
process.env.POD_NAMESPACE = "downward-api";
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.namespace).toBe("downward-api");
|
||||
});
|
||||
|
||||
it("falls back to 'default' when service-account file read throws", async () => {
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.namespace).toBe("default");
|
||||
});
|
||||
|
||||
it("throws when HOSTNAME is not set", async () => {
|
||||
delete process.env.HOSTNAME;
|
||||
await expect(getSelfPodInfo()).rejects.toThrow("HOSTNAME env var not set");
|
||||
});
|
||||
|
||||
it("throws when pod has no spec", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue({ spec: null });
|
||||
await expect(getSelfPodInfo()).rejects.toThrow("has no spec");
|
||||
});
|
||||
|
||||
it("throws when main container has no image", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue({
|
||||
spec: { containers: [{ name: "paperclip", image: "" }] },
|
||||
});
|
||||
await expect(getSelfPodInfo()).rejects.toThrow("has no container image");
|
||||
});
|
||||
|
||||
it("falls back to first container when no container is named 'paperclip'", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue({
|
||||
spec: { containers: [{ name: "other", image: "other:1.0" }] },
|
||||
});
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.image).toBe("other:1.0");
|
||||
});
|
||||
|
||||
it("returns null pvcClaimName when no /paperclip mount exists", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue({
|
||||
spec: { containers: [{ name: "paperclip", image: "p:1", volumeMounts: [] }] },
|
||||
});
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.pvcClaimName).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null pvcClaimName when /paperclip mount is not backed by a PVC", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue({
|
||||
spec: {
|
||||
containers: [{ name: "paperclip", image: "p:1", volumeMounts: [{ name: "data", mountPath: "/paperclip" }] }],
|
||||
volumes: [{ name: "data", emptyDir: {} }],
|
||||
},
|
||||
});
|
||||
const info = await getSelfPodInfo();
|
||||
expect(info.pvcClaimName).toBeNull();
|
||||
});
|
||||
|
||||
it("uses kubeconfig file path when provided (not in-cluster)", async () => {
|
||||
mockReadNamespacedPod.mockResolvedValue(basePod());
|
||||
await getSelfPodInfo("/tmp/kubeconfig.yaml");
|
||||
expect(mockLoadFromFile).toHaveBeenCalledWith("/tmp/kubeconfig.yaml");
|
||||
expect(mockLoadFromCluster).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user