@davidorex/pi-workflows 0.2.0 → 0.3.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 +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +178 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +171 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +63 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +136 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +738 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +457 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +476 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +107 -0
- package/skills/pi-workflows/SKILL.md +185 -0
- package/skills/pi-workflows/references/bundled-resources.md +87 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +94 -0
- package/workflows/create-handoff.workflow.yaml +66 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +81 -0
- package/workflows/plan-from-requirements.workflow.yaml +59 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
package/src/step-transform.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transform step executor — produces output by resolving expressions in a mapping.
|
|
3
|
-
* No LLM call, no subprocess, no shell command — pure expression resolution.
|
|
4
|
-
*/
|
|
5
|
-
import type { TransformSpec, StepResult } from "./types.ts";
|
|
6
|
-
import { resolveExpressions } from "./expression.ts";
|
|
7
|
-
import { zeroUsage } from "./step-shared.ts";
|
|
8
|
-
import { persistStepOutput } from "./output.ts";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Execute a transform step: produces output by resolving expressions in the mapping.
|
|
12
|
-
* No LLM call, no subprocess, no shell command — pure expression resolution.
|
|
13
|
-
*/
|
|
14
|
-
export function executeTransform(
|
|
15
|
-
transform: TransformSpec,
|
|
16
|
-
stepName: string,
|
|
17
|
-
scope: Record<string, unknown>,
|
|
18
|
-
runDir?: string,
|
|
19
|
-
outputPath?: string,
|
|
20
|
-
): StepResult {
|
|
21
|
-
const startTime = Date.now();
|
|
22
|
-
try {
|
|
23
|
-
const output = resolveExpressions(transform.mapping, scope);
|
|
24
|
-
const result: StepResult = {
|
|
25
|
-
step: stepName,
|
|
26
|
-
agent: "transform",
|
|
27
|
-
status: "completed",
|
|
28
|
-
output,
|
|
29
|
-
textOutput: JSON.stringify(output, null, 2),
|
|
30
|
-
usage: zeroUsage(),
|
|
31
|
-
durationMs: Date.now() - startTime,
|
|
32
|
-
};
|
|
33
|
-
if (runDir) {
|
|
34
|
-
result.outputPath = persistStepOutput(runDir, stepName, output, undefined, outputPath);
|
|
35
|
-
}
|
|
36
|
-
return result;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return {
|
|
39
|
-
step: stepName,
|
|
40
|
-
agent: "transform",
|
|
41
|
-
status: "failed",
|
|
42
|
-
usage: zeroUsage(),
|
|
43
|
-
durationMs: Date.now() - startTime,
|
|
44
|
-
error: err instanceof Error ? err.message : String(err),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { createTemplateEnv, renderTemplateFile } from "./template.ts";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
describe("analyzer template inheritance", () => {
|
|
9
|
-
const builtinDir = path.resolve(import.meta.dirname, "..", "templates");
|
|
10
|
-
|
|
11
|
-
it("structure analyzer inherits base and overrides blocks", () => {
|
|
12
|
-
const env = createTemplateEnv("/nonexistent", builtinDir);
|
|
13
|
-
const result = renderTemplateFile(env, "analyzers/structure.md", {});
|
|
14
|
-
assert.ok(result.includes("code structure analyst"));
|
|
15
|
-
assert.ok(result.includes("Architecture"));
|
|
16
|
-
assert.ok(result.includes("Module boundaries"));
|
|
17
|
-
assert.ok(!result.includes("Test coverage"));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("quality analyzer inherits base and overrides blocks", () => {
|
|
21
|
-
const env = createTemplateEnv("/nonexistent", builtinDir);
|
|
22
|
-
const result = renderTemplateFile(env, "analyzers/quality.md", {});
|
|
23
|
-
assert.ok(result.includes("code quality analyst"));
|
|
24
|
-
assert.ok(result.includes("Test coverage"));
|
|
25
|
-
assert.ok(!result.includes("Architecture"));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("pattern analyzer inherits base and overrides blocks", () => {
|
|
29
|
-
const env = createTemplateEnv("/nonexistent", builtinDir);
|
|
30
|
-
const result = renderTemplateFile(env, "analyzers/patterns.md", {});
|
|
31
|
-
assert.ok(result.includes("design pattern analyst"));
|
|
32
|
-
assert.ok(result.includes("Design patterns"));
|
|
33
|
-
assert.ok(result.includes("Anti-patterns"));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("all three produce different output from the same base", () => {
|
|
37
|
-
const env = createTemplateEnv("/nonexistent", builtinDir);
|
|
38
|
-
const s = renderTemplateFile(env, "analyzers/structure.md", {});
|
|
39
|
-
const q = renderTemplateFile(env, "analyzers/quality.md", {});
|
|
40
|
-
const p = renderTemplateFile(env, "analyzers/patterns.md", {});
|
|
41
|
-
// All contain the shared preamble pattern
|
|
42
|
-
assert.ok(s.includes("Given an exploration summary"));
|
|
43
|
-
assert.ok(q.includes("Given an exploration summary"));
|
|
44
|
-
assert.ok(p.includes("Given an exploration summary"));
|
|
45
|
-
// But different identities
|
|
46
|
-
assert.ok(s.includes("structure"));
|
|
47
|
-
assert.ok(q.includes("quality"));
|
|
48
|
-
assert.ok(p.includes("pattern"));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("project template shadows builtin", () => {
|
|
52
|
-
const tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-proj-"));
|
|
53
|
-
const projectTemplates = path.join(tmpProject, ".pi", "templates", "analyzers");
|
|
54
|
-
fs.mkdirSync(projectTemplates, { recursive: true });
|
|
55
|
-
|
|
56
|
-
fs.writeFileSync(path.join(projectTemplates, "structure.md"), [
|
|
57
|
-
'{% extends "analyzers/base-analyzer.md" %}',
|
|
58
|
-
"{% block identity %}You are a CUSTOM structure analyst.{% endblock %}",
|
|
59
|
-
"{% block checklist %}",
|
|
60
|
-
"1. **Custom**: Project-specific check",
|
|
61
|
-
"{% endblock %}",
|
|
62
|
-
].join("\n"));
|
|
63
|
-
|
|
64
|
-
const env = createTemplateEnv(tmpProject, builtinDir);
|
|
65
|
-
const result = renderTemplateFile(env, "analyzers/structure.md", {});
|
|
66
|
-
assert.ok(result.includes("CUSTOM structure analyst"));
|
|
67
|
-
assert.ok(result.includes("Project-specific check"));
|
|
68
|
-
assert.ok(!result.includes("Module boundaries"));
|
|
69
|
-
|
|
70
|
-
fs.rmSync(tmpProject, { recursive: true });
|
|
71
|
-
});
|
|
72
|
-
});
|
package/src/template.test.ts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { createTemplateEnv, renderTemplate, renderTemplateFile } from "./template.ts";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
describe("renderTemplate", () => {
|
|
9
|
-
it("renders variable substitution", () => {
|
|
10
|
-
const env = createTemplateEnv("/nonexistent");
|
|
11
|
-
const result = renderTemplate(env, "Hello {{ name }}, you are {{ role }}", { name: "Alice", role: "analyst" });
|
|
12
|
-
assert.strictEqual(result, "Hello Alice, you are analyst");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("passes through plain text unchanged", () => {
|
|
16
|
-
const env = createTemplateEnv("/nonexistent");
|
|
17
|
-
const result = renderTemplate(env, "No templates here. Just markdown.", {});
|
|
18
|
-
assert.strictEqual(result, "No templates here. Just markdown.");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("renders if/else conditionals", () => {
|
|
22
|
-
const env = createTemplateEnv("/nonexistent");
|
|
23
|
-
const tmpl = "{% if verbose %}Detailed output{% else %}Brief output{% endif %}";
|
|
24
|
-
assert.strictEqual(renderTemplate(env, tmpl, { verbose: true }), "Detailed output");
|
|
25
|
-
assert.strictEqual(renderTemplate(env, tmpl, { verbose: false }), "Brief output");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("renders for loops", () => {
|
|
29
|
-
const env = createTemplateEnv("/nonexistent");
|
|
30
|
-
const tmpl = "{% for item in items %}- {{ item }}\n{% endfor %}";
|
|
31
|
-
const result = renderTemplate(env, tmpl, { items: ["a", "b", "c"] });
|
|
32
|
-
assert.ok(result.includes("- a"));
|
|
33
|
-
assert.ok(result.includes("- b"));
|
|
34
|
-
assert.ok(result.includes("- c"));
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("handles undefined variables gracefully (empty string)", () => {
|
|
38
|
-
const env = createTemplateEnv("/nonexistent");
|
|
39
|
-
const result = renderTemplate(env, "Hello {{ name }}", {});
|
|
40
|
-
assert.strictEqual(result, "Hello ");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("preserves ${{ }} workflow expressions (not interpreted)", () => {
|
|
44
|
-
const env = createTemplateEnv("/nonexistent");
|
|
45
|
-
const result = renderTemplate(env, "Use ${{ steps.explore.output }}", {});
|
|
46
|
-
assert.strictEqual(result, "Use ${{ steps.explore.output }}");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("renders nested object access", () => {
|
|
50
|
-
const env = createTemplateEnv("/nonexistent");
|
|
51
|
-
const result = renderTemplate(env, "File: {{ context.file }}", { context: { file: "main.ts" } });
|
|
52
|
-
assert.strictEqual(result, "File: main.ts");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("renderTemplateFile", () => {
|
|
57
|
-
it("renders a template file from the search path", () => {
|
|
58
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
|
|
59
|
-
fs.writeFileSync(path.join(tmpDir, "test.md"), "Hello {{ name }}");
|
|
60
|
-
|
|
61
|
-
const env = createTemplateEnv("/nonexistent", tmpDir);
|
|
62
|
-
const result = renderTemplateFile(env, "test.md", { name: "World" });
|
|
63
|
-
assert.strictEqual(result, "Hello World");
|
|
64
|
-
|
|
65
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("supports {% extends %} inheritance", () => {
|
|
69
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
|
|
70
|
-
|
|
71
|
-
fs.writeFileSync(path.join(tmpDir, "base.md"), [
|
|
72
|
-
"# {{ title }}",
|
|
73
|
-
"{% block intro %}Default intro{% endblock %}",
|
|
74
|
-
"{% block body %}Default body{% endblock %}",
|
|
75
|
-
].join("\n"));
|
|
76
|
-
|
|
77
|
-
fs.writeFileSync(path.join(tmpDir, "child.md"), [
|
|
78
|
-
'{% extends "base.md" %}',
|
|
79
|
-
"{% block body %}Custom body for {{ focus }}{% endblock %}",
|
|
80
|
-
].join("\n"));
|
|
81
|
-
|
|
82
|
-
const env = createTemplateEnv("/nonexistent", tmpDir);
|
|
83
|
-
const result = renderTemplateFile(env, "child.md", { title: "Report", focus: "security" });
|
|
84
|
-
assert.ok(result.includes("# Report"), "should render parent title");
|
|
85
|
-
assert.ok(result.includes("Default intro"), "should inherit intro block");
|
|
86
|
-
assert.ok(result.includes("Custom body for security"), "should override body block");
|
|
87
|
-
assert.ok(!result.includes("Default body"), "should NOT include default body");
|
|
88
|
-
|
|
89
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("supports {% include %}", () => {
|
|
93
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
|
|
94
|
-
|
|
95
|
-
fs.writeFileSync(path.join(tmpDir, "fragment.md"), "This is a reusable fragment.");
|
|
96
|
-
fs.writeFileSync(path.join(tmpDir, "main.md"), [
|
|
97
|
-
"# Main",
|
|
98
|
-
'{% include "fragment.md" %}',
|
|
99
|
-
"End.",
|
|
100
|
-
].join("\n"));
|
|
101
|
-
|
|
102
|
-
const env = createTemplateEnv("/nonexistent", tmpDir);
|
|
103
|
-
const result = renderTemplateFile(env, "main.md", {});
|
|
104
|
-
assert.ok(result.includes("# Main"));
|
|
105
|
-
assert.ok(result.includes("This is a reusable fragment."));
|
|
106
|
-
assert.ok(result.includes("End."));
|
|
107
|
-
|
|
108
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("supports {% macro %}", () => {
|
|
112
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-test-"));
|
|
113
|
-
|
|
114
|
-
fs.writeFileSync(path.join(tmpDir, "macros.md"), [
|
|
115
|
-
"{% macro greeting(name) %}Hello, {{ name }}!{% endmacro %}",
|
|
116
|
-
].join("\n"));
|
|
117
|
-
fs.writeFileSync(path.join(tmpDir, "main.md"), [
|
|
118
|
-
'{% from "macros.md" import greeting %}',
|
|
119
|
-
"{{ greeting('Alice') }}",
|
|
120
|
-
"{{ greeting('Bob') }}",
|
|
121
|
-
].join("\n"));
|
|
122
|
-
|
|
123
|
-
const env = createTemplateEnv("/nonexistent", tmpDir);
|
|
124
|
-
const result = renderTemplateFile(env, "main.md", {});
|
|
125
|
-
assert.ok(result.includes("Hello, Alice!"));
|
|
126
|
-
assert.ok(result.includes("Hello, Bob!"));
|
|
127
|
-
|
|
128
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("project templates shadow builtin templates", () => {
|
|
132
|
-
const builtinDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-builtin-"));
|
|
133
|
-
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-tmpl-project-"));
|
|
134
|
-
|
|
135
|
-
const projectTemplates = path.join(projectDir, ".pi", "templates");
|
|
136
|
-
fs.mkdirSync(projectTemplates, { recursive: true });
|
|
137
|
-
|
|
138
|
-
fs.writeFileSync(path.join(builtinDir, "shared.md"), "BUILTIN version");
|
|
139
|
-
fs.writeFileSync(path.join(projectTemplates, "shared.md"), "PROJECT version");
|
|
140
|
-
|
|
141
|
-
const env = createTemplateEnv(projectDir, builtinDir);
|
|
142
|
-
const result = renderTemplateFile(env, "shared.md", {});
|
|
143
|
-
assert.strictEqual(result, "PROJECT version");
|
|
144
|
-
|
|
145
|
-
fs.rmSync(builtinDir, { recursive: true });
|
|
146
|
-
fs.rmSync(projectDir, { recursive: true });
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("createTemplateEnv", () => {
|
|
151
|
-
it("creates an environment that renders plain text unchanged", () => {
|
|
152
|
-
const env = createTemplateEnv("/nonexistent");
|
|
153
|
-
const result = renderTemplate(env, "Just plain text", {});
|
|
154
|
-
assert.strictEqual(result, "Just plain text");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("works when no template directories exist", () => {
|
|
158
|
-
const env = createTemplateEnv("/definitely/does/not/exist");
|
|
159
|
-
const result = renderTemplate(env, "Still works: {{ x }}", { x: 42 });
|
|
160
|
-
assert.strictEqual(result, "Still works: 42");
|
|
161
|
-
});
|
|
162
|
-
});
|
package/src/test-helpers.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared test helpers — mock factories for ctx, pi, and workflow specs.
|
|
3
|
-
*/
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import type { WorkflowSpec, StepSpec } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Create a mock extension context for testing.
|
|
11
|
-
*/
|
|
12
|
-
export function mockCtx(cwd: string) {
|
|
13
|
-
return {
|
|
14
|
-
cwd,
|
|
15
|
-
hasUI: false,
|
|
16
|
-
ui: {
|
|
17
|
-
setWidget: () => {},
|
|
18
|
-
notify: () => {},
|
|
19
|
-
setStatus: () => {},
|
|
20
|
-
},
|
|
21
|
-
} as any;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a mock pi API for testing.
|
|
26
|
-
*/
|
|
27
|
-
export function mockPi() {
|
|
28
|
-
const messages: any[] = [];
|
|
29
|
-
return {
|
|
30
|
-
sendMessage: (msg: any, opts: any) => messages.push({ msg, opts }),
|
|
31
|
-
_messages: messages,
|
|
32
|
-
} as any;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create a minimal WorkflowSpec for testing.
|
|
37
|
-
* A fresh temp directory is created for filePath.
|
|
38
|
-
*/
|
|
39
|
-
export function makeSpec(
|
|
40
|
-
overrides: Partial<WorkflowSpec> & { steps: Record<string, StepSpec> },
|
|
41
|
-
): WorkflowSpec {
|
|
42
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-test-"));
|
|
43
|
-
return {
|
|
44
|
-
name: "test-workflow",
|
|
45
|
-
description: "Test workflow",
|
|
46
|
-
version: "1",
|
|
47
|
-
source: "project" as const,
|
|
48
|
-
filePath: path.join(tmpDir, "test.workflow.yaml"),
|
|
49
|
-
...overrides,
|
|
50
|
-
};
|
|
51
|
-
}
|
package/src/tui.test.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
4
|
-
import type { TUI } from "@mariozechner/pi-tui";
|
|
5
|
-
import { createProgressWidget, type ProgressWidgetState } from "./tui.ts";
|
|
6
|
-
import type { WorkflowSpec, StepUsage } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Minimal Theme mock — passes through text unchanged so we can assert on content
|
|
10
|
-
* without dealing with ANSI codes. Mocks exactly the methods tui.ts calls.
|
|
11
|
-
*/
|
|
12
|
-
function mockTheme(): Theme {
|
|
13
|
-
return {
|
|
14
|
-
bold: (s: string) => s,
|
|
15
|
-
fg: (_color: string, s: string) => s,
|
|
16
|
-
dim: (s: string) => s,
|
|
17
|
-
} as unknown as Theme;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Minimal TUI mock — just needs requestRender().
|
|
22
|
-
*/
|
|
23
|
-
function mockTUI(): TUI {
|
|
24
|
-
return {
|
|
25
|
-
requestRender: () => {},
|
|
26
|
-
} as unknown as TUI;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function zeroUsage(): StepUsage {
|
|
30
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function makeWidgetState(overrides: Partial<ProgressWidgetState> = {}): ProgressWidgetState {
|
|
34
|
-
const spec: WorkflowSpec = {
|
|
35
|
-
name: "test-workflow",
|
|
36
|
-
description: "test",
|
|
37
|
-
steps: {
|
|
38
|
-
explore: { agent: "explorer" },
|
|
39
|
-
analyze: { agent: "analyzer" },
|
|
40
|
-
report: { agent: "reporter" },
|
|
41
|
-
},
|
|
42
|
-
source: "project",
|
|
43
|
-
filePath: "/test.yaml",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
spec,
|
|
48
|
-
state: { input: {}, steps: {}, status: "running" },
|
|
49
|
-
startTime: Date.now(),
|
|
50
|
-
...overrides,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("createProgressWidget", () => {
|
|
55
|
-
it("returns a factory function", () => {
|
|
56
|
-
const factory = createProgressWidget(makeWidgetState());
|
|
57
|
-
assert.strictEqual(typeof factory, "function");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("factory creates a component with render and dispose", () => {
|
|
61
|
-
const factory = createProgressWidget(makeWidgetState());
|
|
62
|
-
const component = factory(mockTUI(), mockTheme());
|
|
63
|
-
assert.strictEqual(typeof component.render, "function");
|
|
64
|
-
assert.strictEqual(typeof component.dispose, "function");
|
|
65
|
-
component.dispose!();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("renders header with workflow name and step count", () => {
|
|
69
|
-
const widgetState = makeWidgetState({ currentStep: "explore" });
|
|
70
|
-
const factory = createProgressWidget(widgetState);
|
|
71
|
-
const component = factory(mockTUI(), mockTheme());
|
|
72
|
-
const lines = component.render(120);
|
|
73
|
-
component.dispose!();
|
|
74
|
-
|
|
75
|
-
assert.ok(lines[0].includes("test-workflow"), "header should include workflow name");
|
|
76
|
-
assert.ok(lines[0].includes("1/3"), "header should show step 1/3");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("renders pending steps with dot indicator", () => {
|
|
80
|
-
const widgetState = makeWidgetState();
|
|
81
|
-
const factory = createProgressWidget(widgetState);
|
|
82
|
-
const component = factory(mockTUI(), mockTheme());
|
|
83
|
-
const lines = component.render(120);
|
|
84
|
-
component.dispose!();
|
|
85
|
-
|
|
86
|
-
assert.ok(lines.some(l => l.includes("·") && l.includes("explore")));
|
|
87
|
-
assert.ok(lines.some(l => l.includes("·") && l.includes("analyze")));
|
|
88
|
-
assert.ok(lines.some(l => l.includes("·") && l.includes("report")));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("renders running step with triangle indicator", () => {
|
|
92
|
-
const widgetState = makeWidgetState({ currentStep: "analyze" });
|
|
93
|
-
const factory = createProgressWidget(widgetState);
|
|
94
|
-
const component = factory(mockTUI(), mockTheme());
|
|
95
|
-
const lines = component.render(120);
|
|
96
|
-
component.dispose!();
|
|
97
|
-
|
|
98
|
-
assert.ok(lines.some(l => l.includes("▸") && l.includes("analyze")));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("renders completed step with checkmark, duration, cost, tokens", () => {
|
|
102
|
-
const widgetState = makeWidgetState({ currentStep: "analyze" });
|
|
103
|
-
widgetState.state.steps.explore = {
|
|
104
|
-
step: "explore",
|
|
105
|
-
agent: "explorer",
|
|
106
|
-
status: "completed",
|
|
107
|
-
usage: { input: 5000, output: 2000, cacheRead: 0, cacheWrite: 0, cost: 0.03, turns: 1 },
|
|
108
|
-
durationMs: 42000,
|
|
109
|
-
};
|
|
110
|
-
const factory = createProgressWidget(widgetState);
|
|
111
|
-
const component = factory(mockTUI(), mockTheme());
|
|
112
|
-
const lines = component.render(120);
|
|
113
|
-
component.dispose!();
|
|
114
|
-
|
|
115
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
116
|
-
assert.ok(exploreLine, "should have explore line");
|
|
117
|
-
assert.ok(exploreLine.includes("✓"), "completed should have checkmark");
|
|
118
|
-
assert.ok(exploreLine.includes("42s"), "should show duration");
|
|
119
|
-
assert.ok(exploreLine.includes("$0.03"), "should show cost");
|
|
120
|
-
assert.ok(exploreLine.includes("7.0k tok"), "should show token count");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("renders failed step with X indicator and error", () => {
|
|
124
|
-
const widgetState = makeWidgetState();
|
|
125
|
-
widgetState.state.steps.explore = {
|
|
126
|
-
step: "explore",
|
|
127
|
-
agent: "explorer",
|
|
128
|
-
status: "failed",
|
|
129
|
-
usage: zeroUsage(),
|
|
130
|
-
durationMs: 5000,
|
|
131
|
-
error: "Agent crashed",
|
|
132
|
-
};
|
|
133
|
-
const factory = createProgressWidget(widgetState);
|
|
134
|
-
const component = factory(mockTUI(), mockTheme());
|
|
135
|
-
const lines = component.render(120);
|
|
136
|
-
component.dispose!();
|
|
137
|
-
|
|
138
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
139
|
-
assert.ok(exploreLine, "should have explore line");
|
|
140
|
-
assert.ok(exploreLine.includes("✗"), "failed should have X");
|
|
141
|
-
assert.ok(exploreLine.includes("Agent crashed"), "should show error");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("renders skipped step with circle indicator", () => {
|
|
145
|
-
const widgetState = makeWidgetState();
|
|
146
|
-
widgetState.state.steps.explore = {
|
|
147
|
-
step: "explore",
|
|
148
|
-
agent: "skipped",
|
|
149
|
-
status: "skipped",
|
|
150
|
-
usage: zeroUsage(),
|
|
151
|
-
durationMs: 0,
|
|
152
|
-
};
|
|
153
|
-
const factory = createProgressWidget(widgetState);
|
|
154
|
-
const component = factory(mockTUI(), mockTheme());
|
|
155
|
-
const lines = component.render(120);
|
|
156
|
-
component.dispose!();
|
|
157
|
-
|
|
158
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
159
|
-
assert.ok(exploreLine, "should have explore line");
|
|
160
|
-
assert.ok(exploreLine.includes("⊘"), "skipped should have ⊘");
|
|
161
|
-
assert.ok(exploreLine.includes("[skipped]"), "should show [skipped] tag");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("renders gate step with [gate] tag", () => {
|
|
165
|
-
const widgetState = makeWidgetState();
|
|
166
|
-
widgetState.state.steps.explore = {
|
|
167
|
-
step: "explore",
|
|
168
|
-
agent: "gate",
|
|
169
|
-
status: "completed",
|
|
170
|
-
output: { passed: true, exitCode: 0, output: "ok" },
|
|
171
|
-
usage: zeroUsage(),
|
|
172
|
-
durationMs: 100,
|
|
173
|
-
};
|
|
174
|
-
const factory = createProgressWidget(widgetState);
|
|
175
|
-
const component = factory(mockTUI(), mockTheme());
|
|
176
|
-
const lines = component.render(120);
|
|
177
|
-
component.dispose!();
|
|
178
|
-
|
|
179
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
180
|
-
assert.ok(exploreLine?.includes("[gate]"), "gate step should show [gate] tag");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("renders transform step with [transform] tag", () => {
|
|
184
|
-
const widgetState = makeWidgetState();
|
|
185
|
-
widgetState.state.steps.explore = {
|
|
186
|
-
step: "explore",
|
|
187
|
-
agent: "transform",
|
|
188
|
-
status: "completed",
|
|
189
|
-
output: { merged: true },
|
|
190
|
-
usage: zeroUsage(),
|
|
191
|
-
durationMs: 1,
|
|
192
|
-
};
|
|
193
|
-
const factory = createProgressWidget(widgetState);
|
|
194
|
-
const component = factory(mockTUI(), mockTheme());
|
|
195
|
-
const lines = component.render(120);
|
|
196
|
-
component.dispose!();
|
|
197
|
-
|
|
198
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
199
|
-
assert.ok(exploreLine?.includes("[transform]"), "transform step should show [transform] tag");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("shows parallel count in header for multiple concurrent steps", () => {
|
|
203
|
-
const widgetState = makeWidgetState({ currentStep: "explore, analyze" });
|
|
204
|
-
const factory = createProgressWidget(widgetState);
|
|
205
|
-
const component = factory(mockTUI(), mockTheme());
|
|
206
|
-
const lines = component.render(120);
|
|
207
|
-
component.dispose!();
|
|
208
|
-
|
|
209
|
-
assert.ok(lines[0].includes("[2 parallel]"), "header should show parallel count");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("shows multiple running indicators for parallel steps", () => {
|
|
213
|
-
const widgetState = makeWidgetState({ currentStep: "explore, analyze" });
|
|
214
|
-
const factory = createProgressWidget(widgetState);
|
|
215
|
-
const component = factory(mockTUI(), mockTheme());
|
|
216
|
-
const lines = component.render(120);
|
|
217
|
-
component.dispose!();
|
|
218
|
-
|
|
219
|
-
const runningLines = lines.filter(l => l.includes("▸"));
|
|
220
|
-
assert.strictEqual(runningLines.length, 2, "should have 2 running step indicators");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("renders correct number of lines (header + one per step)", () => {
|
|
224
|
-
const widgetState = makeWidgetState();
|
|
225
|
-
const factory = createProgressWidget(widgetState);
|
|
226
|
-
const component = factory(mockTUI(), mockTheme());
|
|
227
|
-
const lines = component.render(120);
|
|
228
|
-
component.dispose!();
|
|
229
|
-
|
|
230
|
-
// 1 header + 3 steps = 4 lines
|
|
231
|
-
assert.strictEqual(lines.length, 4);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("truncates lines to width", () => {
|
|
235
|
-
const widgetState = makeWidgetState({ currentStep: "explore" });
|
|
236
|
-
const factory = createProgressWidget(widgetState);
|
|
237
|
-
const component = factory(mockTUI(), mockTheme());
|
|
238
|
-
const lines = component.render(20);
|
|
239
|
-
component.dispose!();
|
|
240
|
-
|
|
241
|
-
for (const line of lines) {
|
|
242
|
-
assert.ok(line.length <= 20, `line exceeds width: "${line}" (${line.length})`);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("renders resumed indicator when resumedSteps is set", () => {
|
|
247
|
-
const widgetState = makeWidgetState({ resumedSteps: 3, currentStep: "report" });
|
|
248
|
-
widgetState.state.steps.explore = {
|
|
249
|
-
step: "explore",
|
|
250
|
-
agent: "explorer",
|
|
251
|
-
status: "completed",
|
|
252
|
-
usage: zeroUsage(),
|
|
253
|
-
durationMs: 10000,
|
|
254
|
-
};
|
|
255
|
-
widgetState.state.steps.analyze = {
|
|
256
|
-
step: "analyze",
|
|
257
|
-
agent: "analyzer",
|
|
258
|
-
status: "completed",
|
|
259
|
-
usage: zeroUsage(),
|
|
260
|
-
durationMs: 20000,
|
|
261
|
-
};
|
|
262
|
-
const factory = createProgressWidget(widgetState);
|
|
263
|
-
const component = factory(mockTUI(), mockTheme());
|
|
264
|
-
const lines = component.render(120);
|
|
265
|
-
component.dispose!();
|
|
266
|
-
|
|
267
|
-
const resumedLine = lines.find(l => l.includes("Resumed"));
|
|
268
|
-
assert.ok(resumedLine, "should have a resumed indicator line");
|
|
269
|
-
assert.ok(resumedLine.includes("↻"), "should have ↻ symbol");
|
|
270
|
-
assert.ok(resumedLine.includes("3 steps from prior run"), "should show step count");
|
|
271
|
-
// Header + resumed line + 3 steps = 5 lines
|
|
272
|
-
assert.strictEqual(lines.length, 5);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("renders paused indicator when state status is paused", () => {
|
|
276
|
-
const widgetState = makeWidgetState();
|
|
277
|
-
widgetState.state.status = "paused";
|
|
278
|
-
widgetState.state.steps.explore = {
|
|
279
|
-
step: "explore",
|
|
280
|
-
agent: "explorer",
|
|
281
|
-
status: "completed",
|
|
282
|
-
usage: zeroUsage(),
|
|
283
|
-
durationMs: 10000,
|
|
284
|
-
};
|
|
285
|
-
const factory = createProgressWidget(widgetState);
|
|
286
|
-
const component = factory(mockTUI(), mockTheme());
|
|
287
|
-
const lines = component.render(120);
|
|
288
|
-
component.dispose!();
|
|
289
|
-
|
|
290
|
-
const pausedLine = lines.find(l => l.includes("Paused"));
|
|
291
|
-
assert.ok(pausedLine, "should have a paused indicator line");
|
|
292
|
-
assert.ok(pausedLine.includes("⏸"), "should have ⏸ symbol");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it("does not render resumed indicator when resumedSteps is not set", () => {
|
|
296
|
-
const widgetState = makeWidgetState();
|
|
297
|
-
const factory = createProgressWidget(widgetState);
|
|
298
|
-
const component = factory(mockTUI(), mockTheme());
|
|
299
|
-
const lines = component.render(120);
|
|
300
|
-
component.dispose!();
|
|
301
|
-
|
|
302
|
-
assert.ok(!lines.some(l => l.includes("Resumed")), "should not have resumed indicator");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("shows [truncated] indicator for truncated step", () => {
|
|
306
|
-
const widgetState = makeWidgetState({ currentStep: "analyze" });
|
|
307
|
-
widgetState.state.steps.explore = {
|
|
308
|
-
step: "explore",
|
|
309
|
-
agent: "explorer",
|
|
310
|
-
status: "completed",
|
|
311
|
-
usage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
|
|
312
|
-
durationMs: 5000,
|
|
313
|
-
truncated: true,
|
|
314
|
-
warnings: ["Stdout exceeded 10MB limit"],
|
|
315
|
-
};
|
|
316
|
-
const factory = createProgressWidget(widgetState);
|
|
317
|
-
const component = factory(mockTUI(), mockTheme());
|
|
318
|
-
const lines = component.render(120);
|
|
319
|
-
component.dispose!();
|
|
320
|
-
|
|
321
|
-
// Find the explore line and verify it contains a truncated indicator
|
|
322
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
323
|
-
assert.ok(exploreLine, "explore line should exist");
|
|
324
|
-
assert.ok(exploreLine.includes("truncated"), "Truncated step should show [truncated] indicator");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("does not show [truncated] indicator for non-truncated step", () => {
|
|
328
|
-
const widgetState = makeWidgetState({ currentStep: "analyze" });
|
|
329
|
-
widgetState.state.steps.explore = {
|
|
330
|
-
step: "explore",
|
|
331
|
-
agent: "explorer",
|
|
332
|
-
status: "completed",
|
|
333
|
-
usage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
|
|
334
|
-
durationMs: 5000,
|
|
335
|
-
};
|
|
336
|
-
const factory = createProgressWidget(widgetState);
|
|
337
|
-
const component = factory(mockTUI(), mockTheme());
|
|
338
|
-
const lines = component.render(120);
|
|
339
|
-
component.dispose!();
|
|
340
|
-
|
|
341
|
-
const exploreLine = lines.find(l => l.includes("explore"));
|
|
342
|
-
assert.ok(exploreLine, "explore line should exist");
|
|
343
|
-
assert.ok(!exploreLine.includes("truncated"), "Non-truncated step should not show [truncated]");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("dispose clears the pulse interval", () => {
|
|
347
|
-
const widgetState = makeWidgetState();
|
|
348
|
-
const factory = createProgressWidget(widgetState);
|
|
349
|
-
const component = factory(mockTUI(), mockTheme());
|
|
350
|
-
|
|
351
|
-
// Should not throw; if dispose didn't clear the interval,
|
|
352
|
-
// the test process would hang (node:test waits for timers to drain)
|
|
353
|
-
component.dispose!();
|
|
354
|
-
});
|
|
355
|
-
});
|